使用 GitHub Actions 为 R 包配置持续集成服务
2022年4月11日
编程
2020年的时候我写过一篇《[使用 GitLab Runner 为 R 包配置持续集成服务](https://hpdell.github.io/%E7%BC%96%E7%A8%8B/RPackage-GitlabRunner/)》的博客,主要介绍了在 GitLab 平台上,通过部署 Docker 容器并在中期内安装依赖环境的方法进行R包持续集成服务的配置。随着对 Docker 理解的加深,以及进阶用法的掌握,之前文章中所写的方法已经比较过时了。而且现在 GitHub Actions 已经十分强大,也支持私有库,也支持自己部署 Runners,并提供 macOS 和 Windows 等环境,因此完全可以将 R 包的持续集成方法迁移到 GitHub 上。本文就介绍一下如何使用 GitHub Actions 进行 R 包的持续集成。
# GitHub Actions 配置简介
GitHub Actions 中,也是通过任务(jobs)来指定流水线(workflow)工作的方式。详细说明可以参考[官方文档](https://docs.github.com/en/enterprise-cloud@latest/actions/quickstart)。简单地讲,一个 GitHub Actions 文件(yml 格式)是一个流水线,一个流水线上有很多任务,每个任务有很多步骤,可以运行在不同的操作系统上。因此,一个流水线配置文件一般是这样的:
```yml
name: GitHub Actions Demo
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v3
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ github.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
```
这个文件中,`Explore-GitHub-Actions` 就是名为 `GitHub Actions Demo` 的流水线上的一个任务。这个任务的配置中,通过 `runs-on` 指定任务运行的主机系统,可以选择[由 GitHub 提供的虚拟环境](https://github.com/actions/virtual-environments),也可以在自己的主机上部署一个 Runner,具体的方法可以参考[相关文档](https://docs.github.com/en/enterprise-cloud@latest/actions/hosting-your-own-runners/about-self-hosted-runners)。
> 任务的配置中也可以通过 `container` 选项指定环境,此时这个任务就运行在容器中,而不是主机上。此处我们无需使用这种方式。
每一个任务都由很多步骤(steps)组成,每一个步骤中可以是通过 `run` 选项指定的一系列脚本,也可以通过 `uses` 选项指定一个已经发布的 Action ,或者使用 `uses` 指定一个 Docker 镜像。
* 如果使用 `run` 指定脚本,那么可以后面直接跟一行脚本内容,也可以使用 `|` 引导多行脚本内容。
* 如果使用 `uses` 指定 Action ,格式为 `所有者/仓库名@提交或标签` ,例如 `actions/checkout@v3` 就是官方提供的用于检出仓库的 Action。这里也可以自定义 Action 把一组脚本封装起来,以实现一定的功能。自定义的 Action 既可以由一些命令组成,也可以由 Dockerfile 和相应的脚本文件组成。如果是后者,在运行任务之前会先执行镜像构建。
* 如果使用 `uses` 指定 Docker 镜像,格式为 `docker://镜像标签` 。此时,`runs-on` 必须指定 Linux 环境。镜像可以发布在 Docker Hub 中,也可以发布在 GitHub Packages Container registry 中。
下面我们详细介绍以下各种持续集成的使用方式。
# 基于 actions 的持续集成
这种方式适用于任何系统。在 GitHub Runners 提供的 [macOS](https://github.com/actions/virtual-environments/blob/main/images/macos/macos-11-Readme.md) 和 [Windows](https://github.com/actions/virtual-environments/blob/main/images/win/Windows2022-Readme.md) 环境上,我们可以借助以下几个 action 编写测试:
- [r-lib/actions/setup-r](https://github.com/r-lib/actions) 部署 R 环境
- [r-lib/actions/setup-r-dependicies](https://github.com/r-lib/actions/tree/v2-branch/setup-r-dependencies) 安装 R 依赖库(Linux 需要提前安装第三方依赖库)
- [r-lib/actions/check-r-package](https://github.com/r-lib/actions/tree/v2-branch/check-r-package) 进行 R 包编译和测试
以 macOS 为例,我们可以编写这样一个任务
```yml
jobs:
build_macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Set up R release
uses: r-lib/actions/setup-r@v2
with:
r-version: 'release'
- name: Install R dependencies
uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::rcmdcheck, any::roxygen2
needs: check
- name: R build and check
uses: r-lib/actions/check-r-package@v2
with:
args: 'c("--no-manual", "--as-cran")'
build_args: 'c("--no-manual", "--resave-data")'
error-on: '"error"'
check-dir: '"check"'
```
这种方法的优势是比较方便,泛用性强,即插即用,而且可以复用。但是在 Linux 上,在安装 R 依赖包的时候,由于需要编译安装,需要耗费较长的时间。如果要缩短时间,一种方法是可以使用 cache 功能把依赖包缓存起来。但这样需要耗费较大的空间(GitHub 缓存空间大小有一定限制)。另一种方法是下面介绍的使用 Docker 容器进行持续集成。
# 基于 Docker 的持续集成
由于 macOS 和 Windows 的限制,这种方法基本上只能用于 Linux 系统。目前 CRAN 主要检查 Debian 和 Fedora 两个系统,因此只要创建这两个系统送的镜像即可。这里以 Debian 为例。
## 构建编译环境
在之前的文章中,我们是通过直接部署一个 Debian 的容器,然后在容器控制台中进行环境部署。事实上,这个过程可以通过编写 Dockerfile 完成。在这个 Dockerfile 文件中,我们通过脚本进行 R 的安装与依赖库的安装。
下面是一个示例的 Dockerfile。
```dockerfile
FROM debian:10
ARG DEBIAN_FRONTEND=noninteractive # 设定当前终端为非交互模式,避免 apt 进入交互
ENV TZ=Etc/UTC # 设定当前时区选项,避免在安装相关库时出现时区选择情况
RUN apt-get -qq update && \
apt-get -qq install -y \
gnupg \
build-essential \
gfortran \
devscripts
### 以下两行用于添加 R 镜像源
RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-key '95C0FAF38DB3CCAD0C080A7BDC78B2DDEABC47B7'
RUN echo "deb http://cloud.r-project.org/bin/linux/debian buster-cran40/" >> /etc/apt/sources.list
### 安装 R 以及其他依赖库
RUN apt-get -qq update && \
apt-get -qq install -y \
r-base r-base-dev \
libgsl-dev
libgit2-dev \
libcurl4-openssl-dev \
libxml2-dev \
libssl-dev
### 安装依赖的 R 包
RUN Rscript -e "install.packages(c('devtools', 'Rcpp', 'RcppArmadillo'))"
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]
```
如果要部署 Fedora 的环境,无非就是使用 dnf 包管理器,同时依赖库的安装会发生一些变化。
需要注意的是,这个镜像我们没有指定 `WORKDIR` ,这是因为 GitHub Actions 在运行容器的时候会指定工作目录,且指定为仓库所在的目录。所以这里无需指定工作目录。
## 执行测试
假设我们构建的镜像标签为 `myrpackage-test-debian-release:latest` ,我们可以使用两种方式执行测试。
### 使用 r-libs/action/check-r-package
对于一般的R包,可以使用现成的 [r-libs/action/check-r-package](https://github.com/r-lib/actions/tree/v2-branch/check-r-package) 来执行测试脚本。如果我们使用自定义的容器进行测试,那么就需要让这个 action 运行在我们的容器中。可以使用 `jobs.<job-id>.container` 选项来实现这一点。例如我们可以编写下面这行的 job。
```yml
job:
check-linux:
runs-on: ubuntu-latest
container:
image: myrpackage-test-debian-release:latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Build and Check package
uses: r-lib/actions/check-r-package@v2
with:
args: 'c("--no-manual", "--as-cran")'
build_args: 'c("--no-manual", "--resave-data")'
error-on: '"error"'
check-dir: '"check"'
```
### 使用自定义测试脚本
如果在执行 R 包检查前有一些额外的步骤,那么我们可以将这些步骤连同执行 R 包检查的部分写在 `entrypoint.sh` 的脚本中,在容器创建完成后运行脚本进行检查。下面是一个具体的示例。
```shell
#!/bin/sh -l
mkdir build
cd build
cmake .. -DWITH_R=ON && \
cmake --build . --config Release --target r_package && \
ctest -R Test_R_Package --output-on-failure
exit $?
```
由于这是一个使用 CMake 进行管理的 R 包,所以全都是用 CMake 进行处理的。对于一般的 R 包,就可以写如下脚本
```shell
#!/bin/sh -l
R CMD build PCAKGE && R CMD check PACKAGE_VERSION.tar.gz --as-cran
exit $?
```
只要能够保证工作目录是正确的,并且命令可以运行即可。
由于我们已经通过 Dockerfile 构建好了镜像,在配置流水线任务时,只需要采用 `uses` 选项指定 Docker 镜像即可。当然,首先还是要使用 `actions/checkout@v3` 检出仓库。因此这个任务的编写如下:
```yml
jobs:
build_debian:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: docker://myrpackage-test-debian-release:latest
```
这样,我们就创建了一个由两个步骤组成的任务,第一个步骤检出仓库,第二个步骤构建 Docker 容器并执行相应的操作。由于我们已经在 `entrypoint.sh` 中编写好了操作,容器运行起来后就会执行脚本,以实现持续集成的目的。
# 基于自定义 Runner 的持续集成
事实上,虽然 [r-lib/actions/setup-r](https://github.com/r-lib/actions) 可以在 Windows 上安装 R 环境,但是如果 R 包使用了一些其他库(例如 gsl 等),环境的部署就会非常麻烦了。即使 GitHub Runner 的 Windows 环境安装了 vcpkg 和 conda 等包管理器,但是编译 R 包需要将依赖库安装在 R 的根目录上,使用脚本依然比较困难。但是由于 GitHub 支持自定义 Runner,我们可以在一台 Windows 主机或者虚拟机上部署自定义 Runner 并执行任务。
> 这里说“需要将依赖库安装在 R 的根目录上”,主要是指例如 GSL 等第三方依赖库,需要将这些依赖库安装在 R_HOME 路径下。这在 Windodws 系统上是比较难以做到的,并不是说不行。主要有两种方式:
>
> - 由于 Windows Server 2022 自带了 zip tar gzip 等解压缩工具,可以下载预编译的二进制库并放到 R_HOME 中。
> - 在 RTools 4.2 版本中已经由官方提供了 GSL 等库,所以可以使用 RTools 4.2。
>
> 但如果以上条件并不满足,还是使用自定义的 Runner 进行持续集成比较好。
部署 Runner 的方法很简单,进入仓库的设置界面,选择 Actions > Runner ,点击 New self-hosted runner 按钮,就会出现非常详细的说明。

部署好后,按照以下方法配置任务
```yml
jobs:
build_windows:
runs-on: [self-hosted, Windows, X64]
steps:
- uses: actions/checkout@v3
- name: R build and check
run: |
R CMD build .
R CMD check 包名_版本.tar.gz
```
其中 `runs-on` 中列出的是自定义 Runner 的标签,通过这些标签 GitHub 会选出相应的 Runner 去执行任务。这些标签会在部署时由部署脚本提示输入。
# 流水线触发条件
通常我们可以使用如下触发条件,以支持在 master 分支更新或者有 Pull Request 时进行自动检查。
```yml
on:
push:
branches: [ master ]
tags: [ 'v*.*.*' ]
pull_request:
branches: [ master ]
```
这样的话,就最好遵循 Git 或 GitHub 推荐的工作流进行开发。
如果希望通过流水线自动将 R 包发布在 CRAN 上,可以编写相应的任务,并在任务配置中添加一个条件
```yml
jobs:
upload_cran:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
...
```
这样就只有在打 Tag 的时候才会进行上传。
# 使用 Matrix 在多种环境下执行持续集成
事实上,CRAN 不仅要求 R 包在 Debian Fedora Windows macOS 系统上测试,还要分别测试 release devel old-release 三个 R 版本,甚至还要求分别测试 gcc 和 clang 两种编译器。但是不同版本的 R 进行测试的脚本是一样,只是环境不一样而已。所以我们可以使用 Matrix 让我们的测试运行在不同的环境下。
以 Linux 上测试 release 和 devel 为例,我们的测试需要以下两个维度:
- OS: Debian, Fedora
- R Version: release, devel
那么我们可以这样编写 job
```yml
job:
check-linux:
runs-on: ubuntu-latest
strategy:
matrix:
os: [debian, fedora]
version: [release, devel]
fail-fast: false
container:
image: myrpackage-test-${{ matrix.os }}-${{ matrix.version }}:latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Build and Check package
uses: r-lib/actions/check-r-package@v2
with:
args: 'c("--no-manual", "--as-cran")'
build_args: 'c("--no-manual", "--resave-data")'
error-on: '"error"'
check-dir: '"check"'
```
为了执行这个 job ,我们需要提前构建好以下四个镜像
- myrpackage-test-debian-release:latest
- myrpackage-test-debian-devel:latest
- myrpackage-test-fedora-release:latest
- myrpackage-test-fedora-devel:latest
这样 GitHub 会同时根据我们的配置在以上四个容器中进行测试。
感谢您的阅读。本网站「地与码之间」对本文保留所有权利。