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 packaged 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 custom shell commands. With the introduction of composite Actions, most of the relevant workflow steps for an R project are covered by actions provided by the r-lib/actions repository.

4.2.1.1 Setup

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. As shown in the complete workflow files below:

  • system requirements are installed explicitly (for the ubuntu runner defined for the workflow), based on pkgdepends.
  • dependencies tracked by renv are installed (including caching) using r-lib/actions/setup-renv.

Some remarks about using an renv setup for a packaged Shiny app and a corresponding CI/CD workflow (see also our chapter ‘Control dependencies with renv for details):

  • renv should be configured using "explicit" snapshots, given that dependencies are stated in the DESCRIPTION file.
  • As the DESCRIPTION file defines the development dependencies used for CI/CD, make sure they are tracked by renv via renv::snapshot(dev = TRUE) so they are restored and available in the workflow.
  • Since the deployment to shinyapps.io is also based on renv and would rely on an existing renv.lock, we would end up with development dependencies included in the deployment.
    • To prevent this, we can ignore renv.lock during development by creating an .rscignore text file containing renv.lock in the project root (build-ignored via usethis::use_build_ignore(".rscignore")).
    • This will cause rsconnect::deployApp() to create a new renv snapshot with the run-time dependencies only, still relying on the versions restored from renv.lock in the workflow via r-lib/actions/setup-renv.

4.2.1.2 Package check

4.2.1.3 Deployment

Continuous deployment to shinyapps.io is automated upon any push to the main (or 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 Actions secrets.

  • Deployment to shinyapps.io is best configured and defined in an R script, e.g. deploy/deploy-shinyapps.R (build-ignored via usethis::use_build_ignore("deploy")), which:

    • sets up account credentials based on the environment variables for continuous deployment
    • deploys the app via rsconnect::deployApp(), specifying as files for deployment only what is relevant at run-time: this does not only prevent unnecessary files from being deployed, but also makes sure only run-time dependencies are captured in the deployment

    The script would also be used locally for manual deployments (to rely on the defined files), where credentials would be manually configured.

    # deploy/deploy-shinyapps.R
    # usethis::use_build_ignore("deploy")
    if (!interactive()) {
      rsconnect::setAccountInfo(
        Sys.getenv("SHINYAPPS_ACCOUNT"),
        Sys.getenv("SHINYAPPS_TOKEN"),
        Sys.getenv("SHINYAPPS_SECRET")
      )
    }
    # Add here any additional files/directories the app needs
    app_files = c(
      "app.R",
      "DESCRIPTION",
      "NAMESPACE",
      "R/",
      "inst/"
    )
    rsconnect::deployApp(
      appName = "ShinyCICD", appFiles = app_files, forceUpdate = TRUE
    )

4.2.2 Workflow file

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

# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help

# Name of the workflow
name: CI-CD
# Create status badge with usethis::use_github_actions_badge("ci-cd.yml")

on:
  # Triggered on push and pull request events
  push:
  pull_request:
  # Allow manual runs from the Actions tab
  workflow_dispatch:

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-latest, r: 'release'}

    env:
      # Access token for GitHub
      GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
      # Preserve package sources for informative references in case of errors
      R_KEEP_PKG_SOURCE: yes
      # rsconnect needs transitive LinkingTo-only dependencies to be installed
      # (see https://github.com/r-lib/pak/issues/485)
      PKG_INCLUDE_LINKINGTO: true

    steps:

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

      - name: Setup R
        uses: r-lib/actions/setup-r@v2
        with:
          r-version: ${{ matrix.config.r }}
          # Enable RStudio Package Manager to speed up package installation
          use-public-rspm: true

      - name: Install and cache dependencies
        uses: r-lib/actions/setup-r-dependencies@v2
        with:
          extra-packages: any::rcmdcheck

      - name: Check package
        uses: r-lib/actions/check-r-package@v2

      - 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 the main (or master) branch:

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 = paste0(
  "https://github.com/miraisolutions/techguides/blob/master/",
  "shiny-ci-cd/actions/ci-cd.yml"
  # "shiny-ci-cd/actions/ci-cd-renv.yml"
  # "shiny-ci-cd/actions/ci.yml"
  # "shiny-ci-cd/actions/ci-renv.yml"
))
usethis::use_github_actions_badge("ci-cd.yml") # or "ci.yml"/"ci(-cd)-renv.yml"

4.2.3.1 Complete workflow files

shiny-ci-cd/actions/ci-cd.yml
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help

# Name of the workflow
name: CI-CD
# Create status badge with usethis::use_github_actions_badge("ci-cd.yml")

on:
  # Triggered on push and pull request events
  push:
  pull_request:
  # Allow manual runs from the Actions tab
  workflow_dispatch:

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-latest, r: 'release'}

    env:
      # Access token for GitHub
      GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
      # Preserve package sources for informative references in case of errors
      R_KEEP_PKG_SOURCE: yes
      # rsconnect needs transitive LinkingTo-only dependencies to be installed
      # (see https://github.com/r-lib/pak/issues/485)
      PKG_INCLUDE_LINKINGTO: true

    steps:

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

      - name: Setup R
        uses: r-lib/actions/setup-r@v2
        with:
          r-version: ${{ matrix.config.r }}
          # Enable RStudio Package Manager to speed up package installation
          use-public-rspm: true

      - name: Install and cache dependencies
        uses: r-lib/actions/setup-r-dependencies@v2
        with:
          extra-packages: any::rcmdcheck

      - name: Check package
        uses: r-lib/actions/check-r-package@v2

      - 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
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help

# Name of the workflow
name: CI-CD-renv
# Create status badge with usethis::use_github_actions_badge("ci-cd-renv.yml")

on:
  # Triggered on push and pull request events
  push:
  pull_request:
  # Allow manual runs from the Actions tab
  workflow_dispatch:

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-latest, r: 'renv'}

    env:
      # Access token for GitHub
      GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
      # Preserve package sources for informative references in case of errors
      R_KEEP_PKG_SOURCE: yes

    steps:

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

      - name: Setup R
        uses: r-lib/actions/setup-r@v2
        with:
          r-version: ${{ matrix.config.r }}
          # No RStudio Package Manager to respect renv.lock
          use-public-rspm: false

      - name: Install system dependencies
        # This is not taken care of (yet) by r-lib/actions/setup-renv
        # See https://github.com/r-lib/actions/issues/785
        run: |
          # We rely on pkgdepends from the library embedded in pak
          install.packages("pak", repos = "https://r-lib.github.io/p/pak/stable/")
          install.packages("jsonlite")
          .libPaths(c(system.file("library", package = "pak"), .libPaths()))
          pkgdepends::new_pkg_installation_proposal(
            names(jsonlite::read_json("renv.lock")$Packages), config = list(dependencies = FALSE)
          )$solve()$install_sysreqs()
        shell: Rscript {0}

      - name: Activate renv and restore packages with cache
        uses: r-lib/actions/setup-renv@v2

      - name: Install R CMD check
        run: install.packages("rcmdcheck")
        shell: Rscript {0}

      - name: Check package
        uses: r-lib/actions/check-r-package@v2

      - 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
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help

# Name of the workflow
name: CI
# Create status badge with usethis::use_github_actions_badge("ci.yml")

on:
  # Triggered on push and pull request events
  push:
  pull_request:
  # Allow manual runs from the Actions tab
  workflow_dispatch:

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-latest, r: 'release'}

    env:
      # Access token for GitHub
      GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
      # Preserve package sources for informative references in case of errors
      R_KEEP_PKG_SOURCE: yes
      # rsconnect needs transitive LinkingTo-only dependencies to be installed
      # (see https://github.com/r-lib/pak/issues/485)
      PKG_INCLUDE_LINKINGTO: true

    steps:

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

      - name: Setup R
        uses: r-lib/actions/setup-r@v2
        with:
          r-version: ${{ matrix.config.r }}
          # Enable RStudio Package Manager to speed up package installation
          use-public-rspm: true

      - name: Install and cache dependencies
        uses: r-lib/actions/setup-r-dependencies@v2
        with:
          extra-packages: any::rcmdcheck

      - name: Check package
        uses: r-lib/actions/check-r-package@v2
shiny-ci-cd/actions/ci-renv.yml
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help

# Name of the workflow
name: CI-renv
# Create status badge with usethis::use_github_actions_badge("ci-renv.yml")

on:
  # Triggered on push and pull request events
  push:
  pull_request:
  # Allow manual runs from the Actions tab
  workflow_dispatch:

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-latest, r: 'renv'}

    env:
      # Access token for GitHub
      GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
      # Preserve package sources for informative references in case of errors
      R_KEEP_PKG_SOURCE: yes

    steps:

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

      - name: Setup R
        uses: r-lib/actions/setup-r@v2
        with:
          r-version: ${{ matrix.config.r }}
          # No RStudio Package Manager to respect renv.lock
          use-public-rspm: false

      - name: Install system dependencies
        # This is not taken care of (yet) by r-lib/actions/setup-renv
        # See https://github.com/r-lib/actions/issues/785
        run: |
          # We rely on pkgdepends from the library embedded in pak
          install.packages("pak", repos = "https://r-lib.github.io/p/pak/stable/")
          install.packages("jsonlite")
          .libPaths(c(system.file("library", package = "pak"), .libPaths()))
          pkgdepends::new_pkg_installation_proposal(
            names(jsonlite::read_json("renv.lock")$Packages), config = list(dependencies = FALSE)
          )$solve()$install_sysreqs()
        shell: Rscript {0}

      - name: Activate renv and restore packages with cache
        uses: r-lib/actions/setup-renv@v2

      - name: Install R CMD check
        run: install.packages("rcmdcheck")
        shell: Rscript {0}

      - name: Check package
        uses: r-lib/actions/check-r-package@v2