使用GithubActions自动化工作流
Github在2019年底开放了内置的CI/CD工具GithubActions
.这样使用Github托管的代码终于有了不借助外部服务自动化测试打包部署的能力.
同时由于后发优势,GithubActions
几乎是目前最易用的CI/CD工具.
GithubActions
类似于传统的CI/CD工具,都是使用代码配置脚本,执行器执行脚本,页面管理执行过程的结构.
- 在代码中配置脚本放在根目录的
.github/workflow
文件夹下,使用yaml
格式描述配置. - Github默认给每个用户配置3个的执行器,我们也可以自己创建
self-host
执行器 - 每个代码仓库的顶部标签页都有专门的
actions
按钮,进去就是当前仓库的执行过程管理页面.
对GithubActions
的详细描述可以看官方文档本文只是介绍和划重点.
术语和约束
在介绍使用方式之前我们先来了解下GithubActions
的术语,借此了解下一次执行过程的流程.
workflow
即工作流,一次执行过程.每个workflow用一个配置文件维护.Job
: workflow的分解,可串行存在依赖;可并行Step
: job的分解,即步骤,比如一个step是要给代码做单元测试,那可能会有三个步骤:下载依赖->测试->上传结果action
workflow最小执行单元.即每个执行步骤中的具体执行任务,我们可以自己定义action,也可使用Github社区定义好的actionArtifact
: workflow运行时产生的中间文件.包括日志,测试结果等Event
: 触发workflow的事件
Github Action对workflow
设有如下使用限制:
-
一个仓库可最多同时开20个workflows;超过20则排队等待
-
一个workflow下的每个job最多运行6小时,超过直接结束
-
所有分支下的job根据github级别不同有不同的并行度限制,超过并行度进入队列等待
-
1小时内最多1000次执行请求,也就是1.5api/1m
需要注意,Github对Github Action服务有最终解释权,也就是说乱用可能会被Github限制账户.Github也会生成相关使用统计情况
配置CI/CD
配置CI/CD过程本质上是向runner描述如下内容:
- 什么时候执行
- 执行什么操作.
我们的workflow配置文件也一样是干这个的.
一个典型的workflow配置文件如下:
name: Python package # 定义workflow的名字
# 描述何时执行
on:
push:
branches: [master]
pull_request:
branches: [master]
# 描述workflow要做什么
jobs:
build:
runs-on: ubuntu-latest #描述执行的操作系统
strategy:
matrix: #参数矩阵,每一个元素都会被带入步骤执行
python-version: [3.6, 3.7, 3.8, 3.9]
#描述执行步骤
steps:
- uses: actions/checkout@v4
- name: Set up Python \$\{\{ matrix.python-version \}\}
uses: actions/setup-python@v5
with:
python-version: \$\{\{ matrix.python-version \}\}
- name: Install devDependence
run: |
python -m pip install --upgrade pip
pip install mypy pycodestyle coverage lxml
- name: Install dependencies
run: |
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with pep8
run: |
pycodestyle --max-line-length=140 --ignore=E501 --first --statistics schema_entry
- name: Type Hint Check
run: |
mypy --ignore-missing-imports --show-column-numbers --follow-imports=silent --check-untyped-defs --disallow-untyped-defs --no-implicit-optional --warn-unused-ignores schema_entry
- name: Unit Test
run: |
python -m coverage run --source=schema_entry -m unittest discover -v -s . -p *test*.py
python -m coverage report
workflow的触发
每个workflow的配置文件都需要定义on
字段,它用来描述在何种情况(Event
)下触发执行.我们可以定义on
多种事件,这样只要满足其中一个就会被触发
我们可以将Event
分为3类:
-
定时事件:由定时任务触发的事件
-
手动触发事件: 在actions页面中手动触发的事件
-
Webhook事件:由github网站的钩子行为触发的事件,通常Git操作都有钩子可以用于触发
如果我们希望多种触发都可以生效,可以有两种形式设置:
-
列表形式,注意这种形式中元素只能为字符串
on: - workflow_dispatch - push
-
字典形式,这种形式最常用,其中的元素为字典形式
on: workflow_dispatch: inputs: withpypy: description: 'True to print to STDOUT' required: false default: false type: boolean release: types: [created]
定时事件
最简单的事件就是定时事件其定义方式如下:
on:
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '*/15 * * * *'
上面定义了一个每隔15分钟执行依次的任务.Github Avtion目前只支持crontab语法定义定时任务
这个事件只会拉取默认分支(一般是master
或者main
分支,可以在仓库的settings->branches->Default branch
下修改)的最近一次提交进行执行.
手动触发事件
手动触发事件分为两种:
-
workflow_dispatch
让用在Actions
界面中手动触发workflow 当在workflow中定义了workflow_dispatch
后管理页面就会允许指定这个workflow
被手动执行,执行时默认需要指定分支,如果我们在配置中定义了参数,则手动执行时也会需要填参数.一个典型例子如下:
on: workflow_dispatch: inputs: name: description: 'Person to greet' required: true default: 'Mona the Octocat' home: description: 'location' required: false default: 'The Octoverse' jobs: say_hello: runs-on: ubuntu-latest steps: - run: | echo "Hello \$\{\{ github.event.inputs.name \}\}!" echo "- in \$\{\{ github.event.inputs.home \}\}!"
上面在
workflow_dispatch
下通过定义inputs
设定参数.在jobs
中我们则可以在github.event.inputs
中取到对应的参数. 注意如果不定义手动触发事件那么就无法手动触发. -
repository_dispatch
让用户通过API批量手动执行这个event的主要作用是让其他的程序通过api调用,通过自定义事件类型来驱动执行.这个event对应的workflow必须在默认分支下定义.
比如我们定义:
on: repository_dispatch: types: [opened, deleted]
然后执行http请求:
curl \ -X POST \ -H "Accept: application/vnd.github.v3+json" \ https://api.github.com/repos/{namespace}/{repo_name}/dispatches \ -d '{"event_type":"opened"}'
那么就可以被执行了.其中的
opened
,deleted
是用户自定义的事件.
Webhook事件
Webhook事件是借由Github的webhook事件触发的事件,具体有哪些可以看官方文档,本文将只介绍几个常用的和git操作相关的事件.
-
create
分支,tag创建时触发
-
delete
分支,tag删除时触发
-
gollum
仓库的wiki创建或者更新时触发
-
push
/pull_request
push
是当有对仓库的push操作时触发;pull_request
则是在执行pull request
中触发这两个事件可以额外限制:
branches: [...]
指定符合条件的分支触发branches-ignore:[...]
指定除符合条件的分之外都触发tags:[...]
指定符合条件的tag触发tags-ignore:[...]
指定除符合条件的tag外都触发paths:[...]
代码中有符合条件的路径就触发(至少有一个存在)paths-ignore:[...]
代码中不存在指定的路径则都触发(至少有一个不存在)
上面的限制都允许使用通配符做匹配,支持的通配符包括:
*
: 表示匹配0个或多个非/
字符**
: 表示匹配0个或多个字符.?
: 表示匹配0个或者一个字符+
: 表示匹配至少一个字符[]
: 表示匹配一个范围内的字符,比如[0-9a-f]
表示数字和a到f间的字符可以匹配!
: 在匹配字符串的开头表示否,其他位置没有特殊含义
pull_request
默认的行为是在merge
完成后处理merge
后的那次提交中的代码. 我们还可以通过types: [...]
字段指定细分事件类型,包括:assigned
被分派到某个issue时触发unassigned
删除分派时触发labeled
打标签时触发unlabeled
取消标签时触发opened
创建pull request时触发edited
编辑pull request时触发closed
关闭pull request时触发reopened
重新打开pull request时触发synchronize
同步pull request代码时触发ready_for_review
,pull request处于ready_for_review状态时触发locked
,锁定时触发unlocked
,解锁时触发review_requested
,code review结束时触发review_request_removed
code review请求被删除时触发
-
release
当执行
Github release
时触发,使用的代码时release时打tag的代码,类似于pull_request
,也可以通过types:[...]
来指定细分事件.published
公开后执行unpublished
取消公开后执行created
创建后执行edited
编辑后执行deleted
删除后执行prereleased
预发布后执行released
发布后执行
模板语法
我们可以看到上面例子中会有\$\{\{ ... \}\}
这样的文字,这是Github Action
定义的模板语法,其中...
的部分可以是常数,上下文变量,运算符或者预定义的函数调用.
常数
模板语法支持所有json支持的简单数据类型,也就是null
,boolean
,number
,string
.
上下文变量
每次workflow执行都会带上几个上下文变量用于描述自己和传递参数.具体的可以看官方文档.这边只介绍几个常用的
matrix
,执行策略中定义的变量,每次执行每个key只会有一个取值env
,workflow中env
定义的变量github
,通常用于获取仓库和分支的信息,比较值得关注的有:github.repository
执行的仓库名,也就是{namespace}/{repo_name}
,如果只要repo_name,可以使用${GITHUB_REPOSITORY#*/}
github.ref
工作流的分支或tag,分支为refs/heads/<branch_name>
格式,tag是refs/tags/<tag_name>
格式,如果只要tag名可以使用${GITHUB_REF/refs\/tags\//}
${GITHUB_SHA::8}
可以用于获得前8位的commit的id值github.event.inputs
由手动事件触发传入的参数
secrets
,项目或命名空间定义的账号密码信息,可以在项目的Settings->Secrets
中设置,一般用于上传package或者docker镜像.
运算符
Github Action
支持如下运算符
运算符 | 描述 |
---|---|
() |
逻辑分组 |
[ ] |
索引 |
. |
属性解除参考 |
! |
非 |
< |
小于 |
<= |
小于或等于 |
> |
大于 |
>= |
大于或等于 |
== |
等于 |
!= |
不等于 |
&& |
和 |
\|\| |
或 |
可以看到这些运算符解百纳都是用于做谓词的.因此通常都与if
字段配合使用
steps:
...
- name: The job has failed
if: $
GitHub Action
进行的是宽松的等式比较,其原理是将不同类型的数据转换为数字进行比较:
类型 | 结果 |
---|---|
null |
0 |
true |
返回 1 |
false |
返回 0 |
字符串 |
空字符串为0,符合数字格式的为对应数,否则为NaN |
Array |
NaN,在为同一实例时才视为相等 |
Object |
NaN,在为同一实例时才视为相等 |
注意,类似SQL中的NULL,一个 NaN 与另一个 NaN 的比较不会产生 true.
函数
Github Action支持一些内置函数,比较有用的有:
contains( search, item )
,用于查看序列中是否存在元素startsWith( searchString, searchValue)
/endsWith( searchString, searchValue)
,用于查看字符串中是否已特定字符串开头或者结尾format('Hello {0} {1} {2}', 'Mona', 'the', 'Octocat')
,类似python中的string.format()
,使用模板字符串拼接字符串结果join( array, optionalSeparator )
,类似python中的join,用于拼接数组内容为字符串.- 作业状态检查函数
success()/always()/cancelled()/failure()
,这类函数返回的是bool型数据,因此一般作为谓词与if
联合使用
执行策略
执行策略在一级关键字strategy
中定义.它用于规定执行器执行workflow的行为.主要包括
matrix
,定义执行矩阵,执行器会遍历矩阵执行作业,matrix
中定义的值在执行时可以从上下文matrix
中获取max-parallel
(int)最大并行度fail-fast
(bool,true)快速失败,任何matrix
作业失败,GitHub将取消所有进行中的作业
上面的例子中我们定义了python-version: [3.6, 3.7, 3.8, 3.9]
,这也就意味着执行器会以matrix.python-version
为3.6, 3.7, 3.8, 3.9
分别执行一次.
使用社区定义好的action
可以将action理解为执行过程的封装,使用的人只需要知道它的用法而不需要知道它具体怎么实现的,我们可以自己定义action也可以使用外面定义好的action就像我们编程调用函数一样.社区的actions可以在marketplace找到
上面的例子中我们就使用了一个外部定义好的action:actions/setup-python@v2
使用action
用关键字uses
来声明,如果action需要参数可以使用with
来传入参数
- name: Set up Python \$\{\{ matrix.python-version \}\}
uses: actions/setup-python@v2
with:
python-version: \$\{\{ matrix.python-version \}\}
比较常用的action有:
- actions/checkout@v4,检查workflow
- docker/build-push-action@v5,登录docker 镜像仓库
- docker/setup-qemu-action@v3,设置qemu, 一般用于跨平台编译
- actions/upload-artifact@v3,将
Artifact
发送到workflow的管理界面用于下载 - getsentry/action-release@v1,发送消息到sentry
- jungwinter/split@2,将字符串分割为多段
- actions/setup-python@v4,自动设置python环境
- pypa/cibuildwheel@v2.16.2,python项目打包带扩展的wheel包
- actions/setup-node@v4,设置node环境
- actions/setup-go@v4,设置golang环境
jobs间的依赖关系
当我们单纯定义job时这些job会并行执行,而如果希望明确其中的依赖关系,则可以使用关键字needs
.needs
后的值可以是字符串也可以是字符串为元素的列表
jobs:
build_and_pub_to_pypi:
...
docker-build:
needs: build_and_pub_to_pypi
设置跳过setp
每个setup可以使用keyif
配合谓词来控制是否执行,比如:
steps:
- uses: actions/checkout@v4
# Used to host cibuildwheel
- name: Set up QEMU
if: runner.os == 'Linux'
uses: docker/setup-qemu-action@v3
with:
platforms: all
...
需要注意,只有if
没有else
,因此如果是按一个变量做分支则需要将分支拆成多个if进行判断.
workflow执行器
github默认给每个用户提供了3个的执行器(两核7g内存16g硬盘),如果我们是构造比较大的docker镜像,那16g可能不够用,我们可以使用如下步骤先将预先安装好的永不到的库删掉:
steps:
...
- name: Delete huge unnecessary tools folder
run: rm -rf /opt/hostedtoolcache
...
这个步骤可以删除CodeQL
,Java_Temurin-Hotspot_jdk
,PyPy
,Python
,Ruby
,go
,node
.
这三个执行器的配置是不可变的,但我们可以在仓库的settings->Actions
中配置使用安全策略.
默认的允许所有行为自然是不安全的,但其实一般用问题也不大.当项目是私有项目时我们就需要对action进行限制了.
self-host
我们也可以配置自己的执行器,点击add runner
,进入页面后选好操作系统和平台,然后按指示的配置自己的机器就可以了.
selfhost机器的优势是可以提供更加丰富的配置方式.比如我们做深度学习项目,要用gpu,那可以配一台self-host的gpu机器,然后指定它执行任务;比如我们的部署服务器在内网环境并没有开外网,那么可以让执行器监听外网,然后部署到内网环境,这样也就相对安全了.
使用self-host,只要在配置中一级关键字runs-on
上指定即可
runs-on: [self-hosted, linux, ARM64]
管理和查看workflow
在仓库中我们可以在顶部Actions
标签中管理.
进入后我们可以创建新的workflow,或者查看之前执行过的workflow.
我们可以点击进入某一个workflow中查看详情,在详情页可以重跑任务.如果有上传Artifact也可以在其中下载到