使用 GitHub Actions 构建 CI/CD 流程

GitHub Actions 是一种自动化软件开发工作流的方式,与 GitHub.com 深度集成。开发人员可以通过配置 GitHub Actions 来实现基于事件触发的自动工作流,比如,当有任意用户向 master 分支提交代码时,自动执行一遍完整的单元测试流程等。在本文中,我们展示如何通过配置 GitHub Actions 实现 CI/CD 流程。

一张流程图,左侧部分为一个事件,中间部分是执行器 1,右侧是执行器 2
来自 GitHub Docs 的插图,展示了 GitHub Actions 是基于事件(Event)触发的自动化工作流的抽象

CI/CD,即持续集成(Continuous Integration)、持续部署(Continuous Deployment)与持续交付(Continuous Delivery),是一种更好地交付高质量软件的工作流程。尤其对于个人和小型开发者而言,从项目初期开始即注重软件的交付方式和质量对于提升开发者本身以及用户的体验而言帮助甚大。

Why not Jenkins:基于一般习惯,我们可能更愿意使用 Jenkins 等工具协助搭建 CI/CD 流程。但在使用体验 GitHub Actions 的过程中,我们发现无论是从配置文件的简明程度以及部署和使用的易用性来说,我们更愿意推荐 GitHub Actions 作为在 GitHub.com 上托管项目的 CI/CD 框架。

明确需求

本文中,我们计划使用 GitHub Actions 搭建具有如下特性的自动工作流程:

  • 在每个分支提交代码时,均对代码执行一遍预先定义好的单元测试
  • 提供一个“发布到”按钮,以将主分支(master)上的代码部署到测试/生产环境
  • 代码部署在本地私有服务器(而非 GitHub.com 上的服务器)执行

我们的示例代码位于:https://github.com/DGideas/github-actions-cicd-example

.github/workflows

要使用 GitHub Actions,我们需要在项目的 .github/workflows 文件夹中通过添加描述文件的方式定义相关的工作流程。GitHub Actions 使用 .yaml 格式的描述文件。简言之,每个工作流程通过如下方式进行描述:

  • 在何时触发?
  • 在哪里执行?
    • 是在 GitHub.com 提供的临时虚拟环境里执行,还是在自己本地的私有服务器上执行代码
  • 做什么事情?
    • 通过 .yaml 描述文件的方式,指示当任意时间发生时要完成的事情是什么:运行单元测试,或者给某个邮箱发一封电子邮件

比如,在任意用户为项目提交代码时,一个基于 Python 的项目的自动执行单元测试配置可能是这样的

name: CI
on: push

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repo
        run: |
          rm -rf ${{github.event.repository.name}}
          git clone https://${{github.repository_owner}}:${{github.token}}@github.com/${{github.repository}}
          git -C "${{github.workspace}}/${{github.event.repository.name}}" checkout ${{github.ref}}
      - name: Python unittest
        run: |
          python3 -m venv venv
          source venv/bin/activate
          cd ${{github.event.repository.name}}
          pip3 install -r requirements.txt
          pre-commit run -a
          python3 -m unittest discover -v
      - name: Cleanup
        run: |
          rm -rf venv
          rm -rf ${{github.event.repository.name}}

我们将上述文件提交到项目 .github/workflows/ci.yml 中。

在上述示例中,每当用户向项目的任意分支推送代码时,GitHub Actions 会创建一个运行最新稳定发行版的 Ubuntu 系统的虚拟机,然后在其上运行我们预定义好的操作:检出(checkout)代码、运行单元测试并进行清理操作。清理操作不是必要的,因为由 GitHub.com 托管的虚拟机会在每次执行完任务后自动销毁,但是如果我们在自己托管的本地服务器上运行代码,则每次执行完毕后的清理是有必要的。

一张来自 GitHub.com 的截图,展示了配置好 GitHub Actions 后,每个提交后都会附带 GitHub Actions 脚本的执行状态
配置好 GitHub Actions 后,每当用户向任意分支推送代码,预先定义好的单元测试脚本将会自动运行,并且在 GitHub.com 仓库页面上显示执行状态

点击 “Details” 按钮,还可以看到定义的工作流的具体执行情况:

来自 GitHub.com 的一张截图,展示了 GitHub Actions 页面显示每一次工作流的具体执行情况
GitHub Actions 页面展示的工作流的具体执行情况,如果某个提交为单元测试带来了问题,则可以从这个页面看到具体发生的问题

GitHub Actions 通过获取执行命令的返回值来判断任务是否成功,特别地,在执行命令期间,GitHub Actions 会设置一系列系统环境变量(如 CI),一些开发组件可能依赖这些环境变量,以执行不同的行为。

GitHub Actions 接受的完整配置文件语法请见此处

自己托管主机

在上述示例中,我们使用了来自 GitHub.com 托管的执行器执行 GitHub Actions,但在诸如“将代码发布到服务器”这种场景中,我们需要在自己的主机上执行相应代码。这需要我们将自己的主机添加到 GitHub Actions 中:

  • 在项目“设置”(Settings)中,点选左侧“Actions”,然后选择“Runners”
  • 点击右上角的“New self-hosted runner”,然后选择操作系统,按照提示安装 GitHub Actions Runner 到本地服务器上

安装过程中,我们需要为本地服务器上的 Runner 进行命名,并添加一系列可选的标签(Labels)。这些标签描述了一组具有相似特征的服务器(如生产环境服务器集群)。

对上述示例(ci.yml)中的配置文件稍作修改,我们即可在自己托管的服务器集群上运行单元测试用例:

# ...
jobs:
  ci:
    strategy:
      matrix:
        servers: [ubuntu-latest, macos-latest]
    runs-on: ${{matrix.servers}}
    steps:
# ...

我们使用 matrix 配置项指示上述 Actions 将在 ci-1ci-2ci-3 服务器上同时执行。这些自我托管的服务器可能安装有不同的操作系统,或具有不同的软件环境,以在多种不同的系统环境下运行单元测试代码。

来自 GitHub.com 的一张截图,展示了在多个主机上同时执行脚本的方法
在多个主机上同时执行的 GitHub Actions

接收用户输入

有时我们需要用户主动触发一个脚本,并输入一些执行关键信息。比如,用户可能需要将某个特定分支的代码部署到指定服务器上。我们可以通过 inputs 配置项创建输入表单:

name: 线上部署代码
on:
  workflow_dispatch:
    inputs:
      branchname:
        description: '请输入要部署的分支名称:'
        required: true
        default: 'release'
jobs:
# ...

workflow_dispatch 触发器当且仅当用户通过 GitHub.com 的项目页面进入 Actions 控制面板后,手动运行某个任务时触发:

来自 GitHub.com 的一张截图,展示了通过手动方式运行 GitHub Actions 的方式,在运行特定任务时,可以要求用户通过表单输入任务关键信息
GitHub Actions 在手动运行任务时提供了一个表单,允许用户输入一些执行关键信息

在 .yaml 配置文件中,可以通过 ${{github.event.inputs.branchname}} 的方式在任何位置使用用户对于 branchname 配置项的输入值:

# ...
run: |
  git -C "${{github.workspace}}/${{github.event.repository.name}}" checkout ${{github.event.inputs.branchname}}
# ...

我们同样提供了一个“持续发布”配置文件示例,可以在此处查看。

需要注意的一点是,由于 GitHub Actions Runner 在本地服务器中以独立的用户运行,如果需要更改服务器中的其他文件,可能需要为这些文件或文件夹赋予适当的访问权限,并且可能使用到诸如 setfacl 等命令。

管理秘密变量

在上述 deployment.yml 示例中,每当用户部署成功代码后,我们都会在 Actions 中调用 dawidd6/action-send-mail@v3 以发送电子邮件:

# ...
      - name: Send mail
        uses: dawidd6/action-send-mail@v3
        with:
          server_address: smtp.example.com
          server_port: 587
          username: ${{secrets.MAIL_USERNAME}}
          password: ${{secrets.MAIL_PASSWORD}}
# ...

在 Actions 的每一个执行步骤中使用 uses 而非 run,可以直接复用他人编写的 Actions 而无需重复编写类似命令。如在此示例中,我们使用了 @dawidd6 编写的邮件发送脚本发送电子邮件。我们将发送电子邮件所需的参数通过 with 子句的方式传入到 Actions 中。

细心的读者可能会发现,诸如电子邮箱用户名与密码等信息属于敏感信息,不应该出现在 git 代码库的提交中。GitHub Actions 通过引入秘密变量(Secrets)的概念解决该问题:将不适合提交到代码库中的配置项以秘密变量的形式代替,而将这些变量的真实值通过环境配置的方式写入到项目配置中。用户可以在仓库的“设置”(Settings)中进入“Secrets”查看并设置这些秘密变量,以便在 Actions 中使用。

一旦设置秘密变量,便无法在页面中查看该变量的值。在 Actions 的运行日志中,包含秘密变量在内的敏感字段的值也会被隐去。不推荐将秘密变量的值直接当作参数传入要执行的 shell 命令中,因为此时秘密变量的值会被 ps 命令以及安全审计事件机制等获取。需要使用 stdin 输入或者使用目标进程提供的其他安全机制。

无论是在 GitHub.com 托管的环境运行 Actions,还是在本地私有服务器中运行 Actions,请时刻遵循基本的安全原则。GitHub 提供了一系列 GitHub Actions 常见安全注意事项总结,敬请多加留意。

总结

在本文中,我们介绍了将 GitHub Actions 作为 CI/CD 集成时的用例。通过 GitHub Actions 可以实现各种有意思的应用,如有些用户的个人用户页中甚至嵌入了一个网页版口袋妖怪模拟器,允许访客通过点击按键的方式触发 Actions,推动游戏运行。

另一些 CI/CD 配置时所需的功能,比如“部署审核”——一位用户申请执行某些 Actions 时,需要另一位具有相应权限的用户审核通过后才可执行——属于 GitHub Enterprise 中的高级功能,需要配置相应产品后才可使用。

GitHub Actions 官方文档对于 Actions 的各种详尽配置支持比本文介绍的要多得多,感兴趣的读者可以通过下方的参考资料了解更多信息。

参考资料