Updating an R Package: A Complete Development Workflow
From code changes to CI/CD integration using Git, devtools, and GitHub Actions
r
package-development
git
I did not really understand the full lifecycle of modifying an R package until I had to push a feature branch through CI/CD and watch it either pass or fail on three operating systems. This post walks through the entire process.
Author
Ronald ‘Ryy’ G. Thomas
Published
February 11, 2026
Terminal showing an R package build in progress
Setting up a reproducible R package development workflow.
1 Introduction
I did not really understand how much discipline goes into modifying an R package until I tried to push a change to one of my own projects and watched CI fail on three platforms simultaneously. It turns out that editing an R file is the easy part; the surrounding workflow of branching, documenting, testing, building, and integrating with GitHub Actions is what separates a quick fix from a reliable contribution.
The scenario is straightforward: you have an existing R package hosted on GitHub, and you need to add a feature, fix a bug, or improve error handling. The change itself might be small, but the process of getting it safely merged involves a surprising number of steps.
This post documents the complete workflow I learned while modifying my own package, zzdataframe2graphic. Every step from creating a feature branch to cleaning up after a merge is covered, with the goal of making the process repeatable for future changes.
More formally, this post documents the R-packages layer (Layer 13) of the Workflow Construct described in post 52, specifically the maintenance loop that keeps an existing package healthy across a working biostatistician’s many simultaneous projects. The construct’s R-packages layer holds a small set of author-maintained zz* packages; this post is the how-to-update-one companion to post 35, which is the how-to-author-one entry point.
1.1 Motivations
I kept making ad hoc changes to my R packages without a systematic process, and occasionally broke things on other platforms.
I wanted a clear mental model for the branch-edit-test-merge cycle so I could contribute to open source R projects with confidence.
I had GitHub Actions YAML files in my repositories but did not fully understand what they did or how to set them up from scratch.
I needed a reference I could return to each time I make a package change, rather than re-learning the steps every time.
I wanted to understand how devtools, testthat, and roxygen2 fit together in a single coherent workflow.
1.2 Objectives
Create a feature branch, make code changes, and update roxygen2 documentation in an R package.
Write and run unit tests locally with testthat and build the package with devtools.
Set up three GitHub Actions workflows (R-CMD-check, test coverage, pkgdown) and understand what each one does.
Execute the full pull request lifecycle: push, review, address CI failures, merge, and clean up.
I am documenting my learning process here. If you spot errors or have better approaches, please let me know.
Settling in for a focused development session.
Settling in for a focused development session.
2 Prerequisites and Setup
This workflow assumes familiarity with R, basic Git commands, and a GitHub account. The following tools should be installed:
You also need Git configured on your system and SSH or HTTPS access to your GitHub repositories. The examples below use a package called zzdataframe2graphic, but the process applies to any R package hosted on GitHub.
3 What is an R Package Development Workflow?
An R package development workflow is a structured sequence of steps that takes a code change from an idea to a tested, documented, and merged contribution. Think of it as a checklist for responsible software modification: rather than editing a file and hoping for the best, you create an isolated branch, update documentation automatically, run tests on multiple platforms, and only merge when everything passes.
The key tools in this workflow are devtools (which orchestrates building, testing, and checking), roxygen2 (which generates documentation from inline comments), testthat (which provides a structured testing framework), and GitHub Actions (which runs automated checks on every push). Together, they form a safety net that catches problems before they reach production.
4 Getting Started: Creating a Feature Branch
Every change begins with a new branch. This keeps the main branch stable while you work on your modification.
The first two commands ensure you are starting from the latest version of the codebase. The third creates a new branch and switches to it. All subsequent changes happen on this branch, isolated from main until you are ready to merge.
4.1 Editing the R Code
With the branch created, navigate to the relevant source file and make your changes. For an R package, the source files live in the R/ directory:
Open the target file (e.g., R/zzdataframe2graphic.R).
Make the code changes.
Update or add roxygen2 comments above any modified or new functions.
Save the file.
After editing, regenerate the documentation:
devtools::document()
This command reads your roxygen2 comments and updates the NAMESPACE file and the man/ directory. It is important to run this every time you change function signatures, add parameters, or modify exported functions.
Code review on a laptop screen
Taking a step back to review the changes before testing.
4.2 Writing and Running Tests
Every code change should be accompanied by tests. The testthat framework provides a clean structure for this:
test_that("new feature works as expected", { result <-your_function(test_input)expect_equal(result, expected_output)})
Add your test cases to the appropriate file in tests/testthat/. Then run the full test suite locally:
devtools::test()
Fix any failures before proceeding. If the change is substantial, consider adding multiple test cases covering edge cases and expected error conditions.
4.3 Building and Checking the Package
Once tests pass, build and check the package:
devtools::build()devtools::check()
The check() function runs R CMD check, which is the standard quality gate for R packages. It verifies that documentation is complete, examples run without error, tests pass, and the package can be installed cleanly.
For more rigorous checking, run against additional platforms:
Review the staged changes with git status, then commit with a descriptive message:
git commit -m"feat: add error handling to zzdataframe2graphic- Added input validation for data frame columns- Updated roxygen2 documentation- Added unit tests for edge cases"
5.2 Pushing and Creating a Pull Request
git push origin feature-name
Then create a pull request on GitHub:
Navigate to the repository on GitHub.
Click the “Pull requests” tab.
Click “New pull request.”
Set base to main and compare to feature-name.
Fill in the PR description: what changed, why, how to test, and any related issues.
5.3 GitHub Actions: Automated CI/CD
GitHub Actions automate the testing process every time you push or open a pull request. A typical R package uses three workflows:
R-CMD-check runs R CMD check across multiple operating systems:
Windows (latest)
macOS (latest)
Ubuntu (latest, with R-release, R-devel, and R-oldrel)
This workflow triggers on pushes and pull requests to the main branch.
Test coverage runs all package tests, generates coverage reports, and identifies which parts of the code need additional testing.
pkgdown builds the package documentation website and deploys it to GitHub Pages whenever changes are pushed to the main branch.
5.4 Setting Up GitHub Actions
To add these workflows to a package:
mkdir-p .github/workflows
Then add three YAML configuration files:
.github/workflows/R-CMD-check.yaml
.github/workflows/test-coverage.yaml
.github/workflows/pkgdown.yaml
Configure repository permissions:
Go to Settings, then Actions, then General.
Set “Workflow permissions” to “Read and write.”
Enable GitHub Pages deployment.
The r-lib/actions repository on GitHub provides standard workflow templates for all three.
Run devtools::document() before committing. I have forgotten this step more than once, leading to CI failures because the NAMESPACE file was out of date.
Stage files explicitly rather than using git add . The R build process creates temporary files that should not be committed. Staging specific files avoids accidental inclusion of build artifacts.
Check on multiple platforms. A package that passes on macOS may fail on Windows due to path separators or system dependency differences. The multi-platform R-CMD-check workflow catches these issues.
Keep commits focused. A PR that changes one function, updates its documentation, and adds its tests is easy to review. A PR that touches fifteen files across unrelated features is difficult to evaluate.
Address CI failures immediately. Letting failures accumulate makes debugging harder. Fix each failure as it appears and push the fix before moving on.
8 Uninstall / Rollback
To revert a package change that has not yet been merged, delete the feature branch locally and on the remote.
Reflecting on the lessons from a full development cycle.
9 What Did We Learn?
9.1 Lessons Learnt
Conceptual Understanding:
The R package structure is not just an organizational convenience; it is a contract that R CMD check enforces across documentation, dependencies, and test coverage.
Feature branches isolate risk. Working directly on main means every mistake is immediately visible to collaborators and CI systems.
Automated CI/CD on three operating systems catches platform-specific issues that local testing on a single machine cannot detect.
The pull request is not just a merge mechanism; it is a documentation artifact that records what changed, why, and what tests confirmed the change works.
Technical Skills:
devtools::document() regenerates NAMESPACE and man/ from roxygen2 comments, eliminating manual documentation maintenance.
devtools::check() with --as-cran applies the strictest quality standards and is worth running before every push.
The r-lib/actions repository provides ready-to-use GitHub Actions YAML templates for R-CMD-check, test coverage, and pkgdown.
rcmdcheck::rcmdcheck() provides more detailed output than devtools::check() and is useful for diagnosing obscure failures.
Gotchas and Pitfalls:
Forgetting to run devtools::document() after changing roxygen2 comments causes NAMESPACE mismatches that fail CI silently.
Using git add . can accidentally stage build artifacts, .Rhistory, or .DS_Store files that do not belong in the repository.
GitHub Actions workflows require “Read and write” permissions under repository settings; the default “Read” permission causes pkgdown deployment to fail without a clear error message.
Test coverage workflows may require LaTeX dependencies for vignette building; missing system dependencies produce cryptic errors that do not mention LaTeX directly.
9.2 Limitations
This workflow assumes a single-developer context. Multi-contributor projects require additional conventions around code review, branch protection rules, and merge conflict resolution.
The GitHub Actions templates from r-lib/actions target CRAN-style packages. Packages with non-standard system dependencies may need custom workflow modifications.
Test coverage reports measure which lines of code are executed during tests, not whether the tests are meaningful. High coverage does not guarantee high quality.
The --as-cran check is conservative and may flag issues that are acceptable for internal or non-CRAN packages.
This post does not cover continuous deployment to package repositories (e.g., r-universe or drat) or automated version bumping.
9.3 Opportunities for Improvement
Add pre-commit hooks that run devtools::document() and devtools::test() automatically before each commit.
Integrate lintr into the CI pipeline to enforce consistent code style across all contributions.
Set up branch protection rules on main to require passing CI checks before merging.
Add a NEWS.md file to track user-facing changes with each version increment.
Explore usethis::use_github_action() to generate workflow files directly from R rather than copying YAML templates manually.
Consider adding a code coverage badge to the README to make test coverage visible at a glance.
10 Wrapping Up
The R package development workflow is more involved than simply editing code and pushing to GitHub, but each step exists for a reason. The branching strategy protects the main branch, the documentation tools keep help files synchronized with code, the testing framework catches regressions, and GitHub Actions verify everything works across platforms.
What I learned most from going through this process is that the overhead of a proper workflow pays for itself quickly. The first time CI catches a platform-specific bug that I would never have found on my own machine, the entire setup justified its existence.
For anyone starting out with R package development, my advice is to set up the full workflow once, even for a small package, and then follow it consistently. The steps become automatic after a few iterations.
Main takeaways:
Always branch before making changes, and keep each branch focused on a single feature or fix.
Run devtools::document(), devtools::test(), and devtools::check() locally before pushing.
Set up R-CMD-check, test coverage, and pkgdown GitHub Actions workflows using the templates from r-lib/actions.
Treat the pull request as both a merge mechanism and a documentation record.