GitHub Actions as CI/CD

Published

2026-05-22

WarningCaution

This section is being revised. Thank you for your patience.

In this lab, we’re going to cover using GitHub Actions (or just Actions) for deploying R or Python code (i.e., models, APIs, apps, etc) to a published website. View the original lab file here.

To help explain/demonstrate GitHub Actions, I’ll cover a few examples I use often.

Publishing Quarto/GitHub Pages

This book is published using GitHub Pages, which is a free hosting service provided by GitHub. Quarto provides an easy Terminal command for publishing using GitHub Pages:

quarto publish gh-pages 

The lab for this section will accomplish the same thing, but does so using a GitHub Action workflow file. However, if you run the commands above – as I did – the workflow files don’t exist because the site isn’t published via GitHub Actions. Instead, this repo uses local publishing.

There are two valid ways to publish a Quarto book to GitHub Pages, and running quarto publish gh-pages creates a local setup. This means Quarto renders the files locally, then pushes the rendered HTML directly to the gh-pages branch. The commit log for this branch confirms it:

git log gh-pages --oneline -5
# 76175e88 Built site for gh-pages
# 559df417 Built site for gh-pages
# ...

The Built site for gh-pages messages are Quarto’s signature commit message when it pushes to the branch itself. No workflow file is involved.

To convert this to CI publishing, we’ll have to create and commit .github/workflows/publish.yml and _publish.yml files, so GitHub Actions can render and deploy on every push to main.

Workflow file

In the .github/workflows/publish.yml directory, create …

_publish.yml

Create _publish.yml and include the following …

What they do

%%{init: {'theme': 'neutral', 'look': 'handDrawn', 'themeVariables': { 'fontFamily': 'monospace', "fontSize":"14px"}}}%%
flowchart TD
    Push(["Developer<br>pushes to <code>main</code>"]) -->|"triggers"| GHA["GitHub Actions<br>Runner (ubuntu-latest)"]

    GHA --> Step1("<strong>Step 1</strong><br>Checkout Repository<br><code>actions/checkout@v4</code>")
    Step1 --> Step2("<strong>Step 2</strong><br>Set up Quarto<br><code>quarto-actions/setup@v2</code>")
    Step2 --> Step3("<strong>Step 3</strong><br>Render & Publish<br><code>quarto-actions/publish@v2</code>")

    Step3 -->|"authenticates with"| Token["<cdde>GITHUB_TOKEN</code><br>(auto-provided<br>by GitHub)"]
    Token -->|"writes rendered output"| GHPages("<code>gh-pages</code><br>branch")
    GHPages -->|"served as"| Site(["Live GitHub<br>Pages Site"])

    style Push fill:#F9E79F,stroke:#D4AC0D,color:#000
    style GHA fill:#D4E6F1,stroke:#2E86C1,color:#000
    style Step1 fill:#EBF5FB,stroke:#2E86C1,color:#000
    style Step2 fill:#EBF5FB,stroke:#2E86C1,color:#000
    style Step3 fill:#EBF5FB,stroke:#2E86C1,color:#000
    style Token fill:#FDEDEC,stroke:#C0392B,color:#000
    style GHPages fill:#D5F5E3,stroke:#1E8449,color:#000
    style Site fill:#D5F5E3,stroke:#1E8449,color:#000

Publishing to GitHub pages

Running Tests

You can also use GitHub Actions to run tests on your code. For example, if you have an R package, you can set up a workflow to run testthat tests when code is pushed to the test branch, and only promote to the prod branch if the tests pass.

Workflow file

# stored in .github/workflows/r-package-tests.yml

name: R Package Tests

on:
  push:
    branches: [test]
  pull_request:
    branches: [prod]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Set up R
        uses: r-lib/actions/setup-r@v2
        with:
          use-public-rspm: true

      - name: Install system dependencies
        uses: r-lib/actions/setup-r-dependencies@v2
        with:
          extra-packages: |
            any::rcmdcheck
            any::testthat
            any::devtools

      - name: Run tests
        uses: r-lib/actions/check-r-package@v2
        with:
          error-on: '"error"'

What it does

  • The workflow above triggers on pushes to test (from dev) and on pull requests targeting prod
  • error-on: '"error"' means the workflow only fails on hard errors — warnings won’t block promotion
  • To enforce that tests must pass before merging to prod, enable branch protection rules in GitHub (Settings → Branches → Add rule for prod) and require this workflow as a status check

%%{init: {'theme': 'neutral', 'look': 'handDrawn', 'themeVariables': { 'fontFamily': 'monospace', "fontSize":"14px"}}}%%
flowchart TD
    DevBr(["<code>dev</code> branch"]) -->|"push to test"| TestBr("<code>test</code> branch")
    TestBr -->|"triggers"| GHA["GitHub Actions<br>Run <code>testthat</code> suite"]
    GHA -->|"✅ tests pass"| PR["Pull Request<br><code>test</code> → <code>prod</code>"]
    GHA -->|"❌ tests fail"| Block["Promotion<br>Blocked"]
    PR -->|"approved & merged"| ProdBr("<code>prod</code> branch")

    style DevBr fill:#D4E6F1,stroke:#2E86C1,color:#000
    style TestBr fill:#D5F5E3,stroke:#1E8449,color:#000
    style ProdBr fill:#FADBD8,stroke:#C0392B,color:#000
    style Block fill:#F9E79F,stroke:#D4AC0D,color:#000

Running tests