4.2 GitHub Actions

GitHub Actions is a service for running highly-customizable and flexible automated workflows, fully integrated with GitHub and very suitable to CI/CD pipelines. Workflows use YAML syntax and should be stored in the .github/workflows directory in the root of the repository. Workflows are constituted of jobs and each job is a set of steps to perform individual tasks, e.g. commands or actions.

The next sections describe in detail the relevant workflow steps of a typical CI/CD pipeline for a packages Shiny app, also covering the usage of renv to track package dependencies. Finally, we will show how you can use the convenience function usethis::use_github_action() for including such workflows in you project.

4.2.1 Workflow steps

A workflow should have an identifying name and an on section that indicates upon which events the workflow should be triggered. It should include at least one job and each job will have a set of steps fully specifying what to execute. Such steps can be an action (predefined, sourcing from GitHub repos that contain such actions) or a script. However, for the time being, GitHub Actions does not provide a mature support for hierarchical, aggregated structure of actions. Being entirely customizable, it is necessary to fully specify each step in the CI/CD pipeline explicitly.

4.2.1.1 Setup

  • Checkout the source package from the repository, using actions/checkout provided by GitHub.
  • Setup R using the action r-lib/actions/setup-r.
  • Query and cache R package dependencies using remotes::dev_package_deps() and the actions/cache predefined action.
  • Install system dependencies (for the ubuntu runner defined for the workflow) using sysreqs::sysreq_commands().
  • Install R package dependencies using remotes::install_deps().
Using renv

If your project relies on package renv for tracking dependencies via an renv.lock file, caching and installation of R package dependencies requires a different setup, as described in the Using renv with Continuous Integration vignette and shown in complete workflow files below.

4.2.1.2 Package check

  • Check the package via rcmdcheck::rcmdcheck().

4.2.1.3 Deployment

  • Continuous deployment to shinyapps.io is automated upon any push to the master branch
    • In order to provide credentials for the deployment, account name and corresponding tokens for shinyapps.io are defined as environment variables SHINYAPPS_ACCOUNT, SHINYAPPS_TOKEN and SHINYAPPS_SECRET, specified / accessible as GitHub secrets.
    • A convenience R script, e.g. deploy/deploy-shinyapps.R (build-ignored via usethis::use_build_ignore("deploy")), defines the deployment commands based on the environment variables.
# deploy/deploy-shinyapps.R
# usethis::use_build_ignore("deploy")
rsconnect::setAccountInfo(
  Sys.getenv("SHINYAPPS_ACCOUNT"),
  Sys.getenv("SHINYAPPS_TOKEN"),
  Sys.getenv("SHINYAPPS_SECRET")
)
rsconnect::deployApp(
  appName = "ShinyCICD",
  # exclude hidden files and renv directory (if present)
  appFiles = setdiff(list.files(), "renv")
)

4.2.2 Workflow file

The steps described in the previous section are defined in the .yml workflow file as follows:

# Triggered on push and pull request events
on: [push, pull_request]

# Name of the workflow => usethis::use_github_actions_badge("CI-CD")
name: CI-CD

jobs:
  CI-CD:
    runs-on: ${{ matrix.config.os }}

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

    strategy:
      # we keep a matrix for convenience, but we would typically just run on one
      # single OS and R version, aligned with the target deployment environment
      matrix:
        config:
          - {os: ubuntu-20.04, r: 'release', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"}

    env:
      # Enable RStudio Package Manager to speed up package installation
      RSPM: ${{ matrix.config.rspm }}
      # Access token for GitHub
      GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}

    steps:

      - name: Checkout repo
        uses: actions/checkout@v2

      - name: Setup R
        uses: r-lib/actions/setup-r@v1
        with:
          r-version: ${{ matrix.config.r }}

      - name: Query R dependencies
        run: |
          install.packages('remotes')
          saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2)
          writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version")
        shell: Rscript {0}

      - name: Cache R packages
        uses: actions/cache@v2
        with:
          path: ${{ env.R_LIBS_USER }}
          key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }}
          restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-

      - name: Install system dependencies
        run: |
          while read -r cmd
          do
            eval sudo $cmd
          done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))')

      - name: Install R dependencies
        run: |
          remotes::install_deps(dependencies = TRUE)
          remotes::install_cran("rcmdcheck")
        shell: Rscript {0}

      - name: Check package
        run: |
          options(crayon.enabled = TRUE) # enable colorful R CMD check output
          rcmdcheck::rcmdcheck(args = "--no-manual", error_on = "warning")
        shell: Rscript {0}

      - name: Deploy to shinyapps.io
        # continuous deployment only for pushes to the main / master branch
        if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
        env:
          SHINYAPPS_ACCOUNT: ${{ secrets.SHINYAPPS_ACCOUNT }}
          SHINYAPPS_TOKEN: ${{ secrets.SHINYAPPS_TOKEN }}
          SHINYAPPS_SECRET: ${{ secrets.SHINYAPPS_SECRET }}
        run: Rscript deploy/deploy-shinyapps.R

As visible from the run logs that can be found in the GitHub repository under the Actions tab, all the CI/CD pipeline steps are performed subsequently, and are identifiable by the name field. See the example below, showing how the deployment step is skipped for a run not triggered by a push action on master:

GitHub Actions Continuous Integration / Continuous Deployment pipeline for a packaged Shiny app

4.2.3 Complete workflows and usethis::use_github_action()

Full YAML workflows for CI and CI/CD pipelines, with and without renv, are shown below and provided as part of this guide.

In order to setup and use CI/CD GitHub Actions workflows as described above, you can simply include the relevant workflow file your project via:

usethis::use_github_action(
  url = 
    "https://mirai-solutions.ch/techguides/shiny-ci-cd/actions/ci-cd.yml"
  # "https://mirai-solutions.ch/techguides/shiny-ci-cd/actions/ci-cd-renv.yml"
  # "https://mirai-solutions.ch/techguides/shiny-ci-cd/actions/ci.yml"
  # "https://mirai-solutions.ch/techguides/shiny-ci-cd/actions/ci-renv.yml"
)
usethis::use_github_actions_badge("CI-CD") # or "CI"

4.2.3.1 Complete workflow files

shiny-ci-cd/actions/ci-cd.yml

# Triggered on push and pull request events
on: [push, pull_request]

# Name of the workflow => usethis::use_github_actions_badge("CI-CD")
name: CI-CD

jobs:
  CI-CD:
    runs-on: ${{ matrix.config.os }}

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

    strategy:
      # we keep a matrix for convenience, but we would typically just run on one
      # single OS and R version, aligned with the target deployment environment
      matrix:
        config:
          - {os: ubuntu-20.04, r: 'release', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"}

    env:
      # Enable RStudio Package Manager to speed up package installation
      RSPM: ${{ matrix.config.rspm }}
      # Access token for GitHub
      GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}

    steps:

      - name: Checkout repo
        uses: actions/checkout@v2

      - name: Setup R
        uses: r-lib/actions/setup-r@v1
        with:
          r-version: ${{ matrix.config.r }}

      - name: Query R dependencies
        run: |
          install.packages('remotes')
          saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2)
          writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version")
        shell: Rscript {0}

      - name: Cache R packages
        uses: actions/cache@v2
        with:
          path: ${{ env.R_LIBS_USER }}
          key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }}
          restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-

      - name: Install system dependencies
        run: |
          while read -r cmd
          do
            eval sudo $cmd
          done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))')

      - name: Install R dependencies
        run: |
          remotes::install_deps(dependencies = TRUE)
          remotes::install_cran("rcmdcheck")
        shell: Rscript {0}

      - name: Check package
        run: |
          options(crayon.enabled = TRUE) # enable colorful R CMD check output
          rcmdcheck::rcmdcheck(args = "--no-manual", error_on = "warning")
        shell: Rscript {0}

      - name: Deploy to shinyapps.io
        # continuous deployment only for pushes to the main / master branch
        if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
        env:
          SHINYAPPS_ACCOUNT: ${{ secrets.SHINYAPPS_ACCOUNT }}
          SHINYAPPS_TOKEN: ${{ secrets.SHINYAPPS_TOKEN }}
          SHINYAPPS_SECRET: ${{ secrets.SHINYAPPS_SECRET }}
        run: Rscript deploy/deploy-shinyapps.R

shiny-ci-cd/actions/ci-cd-renv.yml

# Triggered on push and pull request events
on: [push, pull_request]

# Name of the workflow => usethis::use_github_actions_badge("CI-CD")
name: CI-CD

# renv with GitHub actions: https://rstudio.github.io/renv/articles/ci.html#github-actions

jobs:
  CI-CD:
    runs-on: ${{ matrix.config.os }}

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

    strategy:
      # we keep a matrix for convenience, but we would typically just run on one
      # single OS and R version, aligned with the target deployment environment
      matrix:
        config:
          - {os: ubuntu-20.04, r: 'release'}

    env:
      # Root path used by renv and cached
      RENV_PATHS_ROOT: ~/.local/share/renv
      # Access token for GitHub
      GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}

    steps:

      - name: Checkout repo
        uses: actions/checkout@v2

      - name: Setup R
        uses: r-lib/actions/setup-r@v1
        with:
          r-version: ${{ matrix.config.r }}

      - name: Cache R packages
        uses: actions/cache@v2
        with:
          path: ${{ env.RENV_PATHS_ROOT }}
          key: ${{ runner.os }}-renv-${{ hashFiles('**/renv.lock') }}
          restore-keys: |
            ${{ runner.os }}-renv-

      - name: Install system dependencies
        run: |
          Rscript -e "install.packages('remotes')"
          while read -r cmd
          do
            eval sudo $cmd
          done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))')

      - name: Restore packages
        run: |
          renv::restore()
        shell: Rscript {0}

      - name: Check package
        run: |
          install.packages("rcmdcheck")
          options(crayon.enabled = TRUE) # enable colorful R CMD check output
          rcmdcheck::rcmdcheck(args = "--no-manual", error_on = "warning")
        shell: Rscript {0}

      - name: Deploy to shinyapps.io
        # continuous deployment only for pushes to the main / master branch
        if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
        env:
          SHINYAPPS_ACCOUNT: ${{ secrets.SHINYAPPS_ACCOUNT }}
          SHINYAPPS_TOKEN: ${{ secrets.SHINYAPPS_TOKEN }}
          SHINYAPPS_SECRET: ${{ secrets.SHINYAPPS_SECRET }}
        run: Rscript deploy/deploy-shinyapps.R

shiny-ci-cd/actions/ci.yml

# Triggered on push and pull request events
on: [push, pull_request]

# Name of the workflow => usethis::use_github_actions_badge("CI-CD")
name: CI-CD

jobs:
  CI-CD:
    runs-on: ${{ matrix.config.os }}

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

    strategy:
      # we keep a matrix for convenience, but we would typically just run on one
      # single OS and R version, aligned with the target deployment environment
      matrix:
        config:
          - {os: ubuntu-20.04, r: 'release', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"}

    env:
      # Enable RStudio Package Manager to speed up package installation
      RSPM: ${{ matrix.config.rspm }}
      # Access token for GitHub
      GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}

    steps:

      - name: Checkout repo
        uses: actions/checkout@v2

      - name: Setup R
        uses: r-lib/actions/setup-r@v1
        with:
          r-version: ${{ matrix.config.r }}

      - name: Query R dependencies
        run: |
          install.packages('remotes')
          saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2)
          writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version")
        shell: Rscript {0}

      - name: Cache R packages
        uses: actions/cache@v2
        with:
          path: ${{ env.R_LIBS_USER }}
          key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }}
          restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-

      - name: Install system dependencies
        run: |
          while read -r cmd
          do
            eval sudo $cmd
          done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))')

      - name: Install R dependencies
        run: |
          remotes::install_deps(dependencies = TRUE)
          remotes::install_cran("rcmdcheck")
        shell: Rscript {0}

      - name: Check package
        run: |
          options(crayon.enabled = TRUE) # enable colorful R CMD check output
          rcmdcheck::rcmdcheck(args = "--no-manual", error_on = "warning")
        shell: Rscript {0}

shiny-ci-cd/actions/ci-renv.yml

# Triggered on push and pull request events
on: [push, pull_request]

# Name of the workflow => usethis::use_github_actions_badge("CI-CD")
name: CI-CD

# renv with GitHub actions: https://rstudio.github.io/renv/articles/ci.html#github-actions

jobs:
  CI-CD:
    runs-on: ${{ matrix.config.os }}

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

    strategy:
      # we keep a matrix for convenience, but we would typically just run on one
      # single OS and R version, aligned with the target deployment environment
      matrix:
        config:
          - {os: ubuntu-20.04, r: 'release'}

    env:
      # Root path used by renv and cached
      RENV_PATHS_ROOT: ~/.local/share/renv
      # Access token for GitHub
      GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}

    steps:

      - name: Checkout repo
        uses: actions/checkout@v2

      - name: Setup R
        uses: r-lib/actions/setup-r@v1
        with:
          r-version: ${{ matrix.config.r }}

      - name: Cache R packages
        uses: actions/cache@v2
        with:
          path: ${{ env.RENV_PATHS_ROOT }}
          key: ${{ runner.os }}-renv-${{ hashFiles('**/renv.lock') }}
          restore-keys: |
            ${{ runner.os }}-renv-

      - name: Install system dependencies
        run: |
          Rscript -e "install.packages('remotes')"
          while read -r cmd
          do
            eval sudo $cmd
          done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))')

      - name: Restore packages
        run: |
          renv::restore()
        shell: Rscript {0}

      - name: Check package
        run: |
          install.packages("rcmdcheck")
          options(crayon.enabled = TRUE) # enable colorful R CMD check output
          rcmdcheck::rcmdcheck(args = "--no-manual", error_on = "warning")
        shell: Rscript {0}