Continuous integration for R projects: from Travis CI to GitHub actions step by step

November 24, 2020

  Continuous Integration Travis CI GitHub Actions R package
  covr pkgdown usethis

Kevin Cazelles  

Marie-Hélène Brice   Willian Vieira  

      


I have been using Travis CI (https://travis-ci.org/) since 2015 and GitHub Actions (hereafter GH Actions; https://github.com/features/actions) for over a year now1. While using these two hosted continuous integration services, I came to realize that I was spending less time troubleshooting with GH Actions than I was with Travis. For this reason, I am now setting GH Actions workflows for my new projects (mainly R and Julia projects) and slowly migrating older projects from Travis to GH Actions. Turns out that was the right move! Indeed, as Jeroen Ooms explains in his recent post “Moving away from Travis CI”, by next year, Travis will no longer be free for open source projects (see also), which gives me a good reason to speed up the migration process! As I was working on such migration recently, I felt that I should to share some notes about this!

inSilecoMisc as an example

The package I worked on was inSilecoMisc. Note that I have already detailed elsewhere various features including in inSilecoMisc (e.g. here and there). What matters here, is what was Travis CI doing for me. The screenshot below shows the different jobs I set (described in .travis.yml).

><

Screenshot of the inSilecoMisc project page on travis-ci.org.

Travis was set to check inSilecoMisc on macOS and Linux (Ubuntu Bionic and Focal), for different versions of R (oldrel, rel and devel). The last job, checked the package on Ubuntu Focal, uploaded covr results to CodeCov, built inSilecoMisc’s website (via pkgdown) and deploy to a GitHub Pages. So my goal was to set up a workflow with GitHub Actions that would do the same jobs.

Ciao Travis!

The first step was to stop using Travis CI. To do so, I first needed to go on https://travis-ci.org and turn off the jobs described above, so I looked for inSilecoMisc in my list of projects and switched it off (see screenshot below).

><

Screenshots of inSilecoMisc’s status on https://travis-ci.org (left: on/before, right: off/after).

Then, as I was using a deploy key for the website, I deleted it in the settings on GitHub repository (). Last, on my local repository I removed .travis.yaml (e.g. git rm .travis.yaml). Note that I still have a version of this file as a gist. Once this was done, I committed my changes and pushed!

><

Screenshot of inSileco’s home page (commit 500d5ef).

Add a workflow

GH Actions are well-documented and my goal here is not to explain how to set an entire workflow. Rather I would like to focus on certain part of the workflow. That said, one should keep in mind that thanks to Docker images and the large diversity of Actions available, GH Actions are powerful and extremely flexible. For R users interested in using GH Actions to check their package, it is worth noting that usethis has several functions to add such workflows:

library(usethis)
use_github_action() 
# there are different level of completeness, see the documentation 
# and select the one you need! 
use_github_action_check_release()
use_github_action_check_standard()
use_github_action_check_full()

The workflow I ended up using, R-CMD-check.yaml (appended at the end of the post) is based on one of the template file available in usethis that I’ve simplified a bit (I use only 4 combinations OS / R versions) and extended to use the CodeCov and deploy the website.

Code coverage

To use CodeCov with Actions, a token is required. So, I clicked on the Settings tab of the inSilecoMisc page and copied the upload token:

><

Screenshot of the CodeCov settings page of inSilecoMisc.

Then I created a new secret CODECOV_TOKEN on inSileco’s GitHub repository and I pasted the token.

><

Screenshot of the Secret tab of the setting of the GitHub inSilecoMisc repository.

I chose CODECOV_TOKEN because it’s pretty clear, but the name does not matter as long as the same variable name is used in R-CMD-check.yaml. Below is the code bloc in the workflow that handles code coverage:

- name: Test coverage
  if: matrix.config.os == 'macOS-latest' && matrix.config.r == 'release'
  run: |
    remotes::install_cran("covr")
    covr::codecov(token = "${{secrets.CODECOV_TOKEN}}")
  shell: Rscript {0}

Note that ${{secrets.CODECOV_TOKEN}} returns the token value I set as a secret above. Also, I used an if: filed to upload the results of the code coverage only once. I chose to do it on MacOS (for no specific reason) and I kept the part matrix.config.r == 'release' in case I would add another configuration (currently, this is useless).

Website

Build

In order to build the package I simply install pkgdown and use it to generates the HTML pages. Below is the code bloc I wrote for this:

- name: Build website 
  if: matrix.config.os == 'ubuntu-20.04' && matrix.config.r == 'release'
  run: |
    mkdir docs
    sudo apt-get install libcurl4-openssl-dev libharfbuzz-dev libfribidi-dev
    git fetch origin gh-pages:gh-pages
    git --work-tree=docs checkout gh-pages -- .
    Rscript -e 'remotes::install_cran(c("pkgdown")); pkgdown::build_site()'

There are a few lines that require some explanations. First, I decided to build this on Ubuntu (again, this is arbitrary). Second, the following line:

sudo apt-get install libcurl4-openssl-dev libharfbuzz-dev libfribidi-dev

was required as dependencies were missing for curl and textshaping. Third, the two last lines are two git operations that I used to retrieve te content of the previous commit. I did so to be able to have both the “main” website at https://insileco.github.io/inSilecoMisc/ and “dev” version at https://insileco.github.io/inSilecoMisc/dev (this is detailed in another post “Deploy a pkgdown website on gh-pages manually”).

Deploy

To deploy the website to GitHub Pages, I used peaceiris/actions-gh-pages (version 3). There are two ways to deploy the website that are well explained in the README of peaceiris/actions-gh-pages.

via a deploy key

For this, one needs to generate a pair of ssh keys. On my Debian machine, I used the following command line in my terminal:

$ ssh-keygen -t rsa -b 4096 -C "$(git config user.email)" -f gh-pages

Then I set the public key as a deploy key

><

Screenshot of the deploy key setting.

and the private one as a secret.

><

Screenshot of the secret ACTIONS_DEPLOY_KEY.

Again, I chose ACTIONS_DEPLOY_KEY for its clarity, but the name does not matter as long as the correct variable name is used in R-CMD-check.yaml.

- name: Deploy website 
  if: matrix.config.os == 'ubuntu-20.04' && matrix.config.r == 'release'
  uses: peaceiris/actions-gh-pages@v3
  with:
    deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
    publish_dir: ./docs

I used publish_dir: ./docs as this is where pkgdown stores stored the web pages in my case (see the file _pkgdown.yml). Note that I didn’t need to specify the branch to be used for the website as the default is gh-pages and I already had a branch gh-pages set up2.

via a personal access token

The second option is to create and use a personal access token (this is the solution I opted for in the end).

><

Screenshot of the page to generate the personal token.

And then set this token as a secret for the repository.

><

Screenshot of the secret PERSO_TOKEN.

Once the secret is set, the variable name should be passed as github_token (instead of deploy_key), like so:

- name: Deploy website 
  if: matrix.config.os == 'ubuntu-20.04' && matrix.config.r == 'release'
  uses: peaceiris/actions-gh-pages@v3
  with:
    github_token: ${{ secrets.PERSO_TOKEN }}
    publish_dir: ./docs

Incidentally, organization secrets allow user to have secrets at the organisation level, so that the same token can be used for several repositories!


Everything is working fine now 🎆, check out inSilecoMisc! Below is the entire workflow

Entire workflow

Below is the whole R-CMD-check.yaml file I ended using (note that for recent projects the default branch name is main instead of master).

on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master

name: R-CMD-check

jobs:
  R-CMD-check:
    runs-on: ${{ matrix.config.os }}

    name: ${{ matrix.config.os }} (${{ matrix.config.r }})

    strategy:
      fail-fast: false
      matrix:
        config:
          - {os: windows-latest, r: 'release'}
          - {os: macOS-latest, r: 'release'}
          - {os: ubuntu-20.04, r: 'release', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"}
          - {os: ubuntu-20.04, r: 'devel', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"}

    env:
      R_REMOTES_NO_ERRORS_FROM_WARNINGS: true
      CRAN: ${{ matrix.config.cran }}

    steps:
      - uses: actions/checkout@v1

      - uses: r-lib/actions/setup-r@master
        with:
          r-version: ${{ matrix.config.r }}

      - uses: r-lib/actions/setup-pandoc@master

      - name: Query dependencies
        run: |
          install.packages('remotes')
          saveRDS(remotes::dev_package_deps(dependencies = TRUE), "depends.Rds", version = 2)
        shell: Rscript {0}

      - name: Cache R packages
        if: runner.os != 'Windows'
        uses: actions/cache@v1
        with:
          path: ${{ env.R_LIBS_USER }}
          key: ${{ runner.os }}-r-${{ matrix.config.r }}-${{ hashFiles('depends.Rds') }}
          restore-keys: ${{ runner.os }}-r-${{ matrix.config.r }}-

      - name: Install system dependencies
        if: runner.os == 'Linux'
        env:
          RHUB_PLATFORM: linux-x86_64-ubuntu-gcc
        run: |
          Rscript -e "remotes::install_github('r-hub/sysreqs')"
          sysreqs=$(Rscript -e "cat(sysreqs::sysreq_commands('DESCRIPTION'))")
          sudo -s eval "$sysreqs"
      - name: Install dependencies
        run: |
          library(remotes)
          deps <- readRDS("depends.Rds")
          deps[["installed"]] <- vapply(deps[["package"]], remotes:::local_sha, character(1))
          update(deps)
          remotes::install_cran("rcmdcheck")
        shell: Rscript {0}

      - name: Check
        run: rcmdcheck::rcmdcheck(args = "--no-manual", error_on = "warning", check_dir = "check")
        shell: Rscript {0}

      - name: Upload check results
        if: failure()
        uses: actions/upload-artifact@master
        with:
          name: ${{ runner.os }}-r${{ matrix.config.r }}-results
          path: check

      - name: Test coverage
        if: matrix.config.os == 'macOS-latest' && matrix.config.r == 'release'
        run: |
          remotes::install_cran("covr")
          covr::codecov(token = "${{secrets.CODECOV_TOKEN}}")
        shell: Rscript {0}
    
      - name: Build website 
        if: matrix.config.os == 'ubuntu-20.04' && matrix.config.r == 'release'
        run: |
          sudo apt-get install libcurl4-openssl-dev libharfbuzz-dev libfribidi-dev
          mkdir docs
          git fetch origin gh-pages:gh-pages
          git --work-tree=docs checkout gh-pages -- .
          Rscript -e 'remotes::install_cran(c("pkgdown")); pkgdown::build_site()'
            
      - name: Deploy website 
        if: matrix.config.os == 'ubuntu-20.04' && matrix.config.r == 'release'
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.PERSO_TOKEN }}
          publish_dir: ./docs

Hope this could be of help 😄!



  1. see my first post about GitHub Actions on my personal blog.↩︎

  2. Use publish_branch: your-branch to set your-branch as the branch were files will be published.↩︎

Edits

May 21, 2021 -- Edit the tricks to keep both the versions of the website (dev and last releases).