使用GithubActions自动化工作流

Posted by Hsz on November 30, 2020

使用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是要给代码做单元测试,那可能会有三个步骤:下载依赖->测试->上传结果
  • actionworkflow最小执行单元.即每个执行步骤中的具体执行任务,我们可以自己定义action,也可使用Github社区定义好的action
  • Artifact: 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@v2
      - name: Set up Python \$\{\{ matrix.python-version \}\}
        uses: actions/setup-python@v2
        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操作都有钩子可以用于触发

如果我们希望多种触发都可以生效,可以有两种形式设置:

  1. 列表形式,注意这种形式中元素只能为字符串

    
     on:
       - workflow_dispatch
       - push
    
    
  2. 字典形式,这种形式最常用,其中的元素为字典形式

     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_removedcode 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-version3.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有:

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标签中管理. actions

进入后我们可以创建新的workflow,或者查看之前执行过的workflow. 管理仓库的actions

我们可以点击进入某一个workflow中查看详情,在详情页可以重跑任务.如果有上传Artifact也可以在其中下载到 详情