# install.packages('pak')
::pak('mjfrigaard/shinypak') pak
21 GitHub Actions
Continuous Integration (CI) and Continuous Deployment (CD) help automate software development tasks, especially testing and deployment. In the context of a Shiny app-package, CI/CD usually refers to GitHub Actions or Travis CI.1
Continuous integration
Automated Testing: CI can automate the tests (
testthat
andshinytest2
) for the code in your app-package, which is helpful when pushing a new feature or bug fix.Quality Assurance: CI helps maintain the code quality by running a series of ‘checks and balances’ to confirm updates and changes don’t break anything. Should something go wrong, CI will alert developers so they can fix it promptly.
Change Management: CI systems work hand-in-hand with version control systems (like Git), which makes it possible to track all the changes within a project. This makes sure every modification is checked and is safe to be included in the app-package pushed to the
main
branch.
Continuous deployment
Automated Deployment: Once CI confirms the changes are safe, CD can automatically deploy your updated Shiny app-package to a package management system or Shiny server, which means that the updated version becomes available to users without any manual intervention.
Consistent Updating: CD ensures that users experience new features and fixes much faster. Improvements are deployed as soon as they are made and tested, which leads to a more responsive development process.
Streamlined Development: CI and CD streamline the software development process so developers can focus more on building and less on testing and deploying.
Properly implemented CI/CD is like a well-oiled machine, where each part works seamlessly with the others, which saves time, reduces errors, and makes the process efficient and reliable.
GitHub Actions
GitHub Actions (or Actions, for short) is a feature provided by GitHub that enables automation of various development workflows. These workflows are defined in YAML files and can automate some of the repetitive tasks in the development lifecycle, right within a GitHub repository.
Actions allow us to create custom workflows to automatically build, test, and deploy our code. Workflows can be triggered by a push to a branch, a pull request, a specific time schedule, or another GitHub event.
The workflow file
The YAML workflow file configures and defines the automated tasks run in a GitHub repository. Workflows typically include fields for building, testing, and deploying code, and are structured into several key sections:
on
“An event is a specific activity in a repository that triggers a workflow run.”
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
on
defines the event(s) that trigger the workflow.2
name
name: shiny
name
is an optional field to identify a workflow in the Actions tab of the GitHub repository.3
jobs
“A job is a set of steps in a workflow that is executed on the same runner.”
jobs:
check:
runs-on: ubuntu-latest
- The
job
and it’s identifier (check
) are used to reference the runner, in this example it’subuntu-latest
.
jobs:
check:
runs-on: ${{ matrix.config.os }}
matrix
runs jobs across different operating systems, programming language versions, etc.
name: ${{ matrix.config.os }} (${{ matrix.config.r }})
strategy:
fail-fast: false
matrix:
config:
- {os: macos-latest, r: 'release'}
- {os: windows-latest, r: 'release'}
- {os: ubuntu-latest, r: 'release'}
fail-fast: false
means the workflow will continue running even if one of thematrix
jobs fails.
steps
“Steps can run commands, run setup tasks, or run an action in your repository, a public repository, or an action published in a Docker registry. Not all steps run actions, but all actions run as a step.”
steps:
- uses: actions/checkout@v2
- name: Set up R
uses: r-lib/actions/setup-r@v2
- name: Install package
run: |
install.packages('remotes')
remotes::install_local('.') shell: Rscript {0}
steps
define a series of tasks or actions that thejob
will execute. Thesesteps
check out the code, set up an R environment, and installs a local package
Workflow permissions
In order for workflows to run, we’ll need to make sure Actions have read and write permissions. We can do this by clicking on the repository’s Settings tab, then expand the Actions menu and select General:
In the General settings, we want to confirm the workflow has Read and write permissions permissions (if this needs to be been changed, be sure to click Save).
Now our workflows can commit and push changes to the repository.
21.1 Code styling and linting
In R packages, we can set up the GitHub Action infrastructure with usethis::use_github_action()
. The name
can be any of the workflow files available at r-lib/actions. The first workflow we’ll be demonstrating can be used to automate the code style in an app-package.
Launch app with the shinypak
package:
launch('16.1_gha-style')
::use_github_action("style") usethis
✔ Setting active project to '/projects/apps/sap'
✔ Creating '.github/'
✔ Adding '*.html' to '.github/.gitignore'
✔ Creating '.github/workflows/'
✔ Saving 'r-lib/actions/examples/style.yaml@v2' to '.github/workflows/style.yaml'
The output tells us a local .github/workflows/style.yaml
file has been copied from the r-lib/actions/examples/style.yaml@v2
into the .github/workflows
folder:
.github/
└── workflows
└── style.yaml
2 directories, 1 file
The documentation for the style.yaml@v2
file tells us this workflow,
“styles the R code in a package, then commits and pushes the changes to the same branch.”
The code styling in this workflow is handled by the styler
package, which “formats your code according to the tidyverse style guide.”
Three functions in the 16.1_gha-style
branch have altered to have zero style (R/display_type.R
, R/mod_var_input.R
, and R/test_logger.R
). You can copy the code for these functions in the chunk below:4
show/hide no-style code
# display_type ----
<- function(run = "w") {
display_type if(Sys.getenv("RSTUDIO") == "1"){
switch(run,
p=options(shiny.launch.browser=.rs.invokeShinyPaneViewer),
b=options(shiny.launch.browser=.rs.invokeShinyWindowExternal),
w=options(shiny.launch.browser=.rs.invokeShinyWindowViewer),
NULL=options(shiny.launch.browser=NULL))
<- "RStudio"
environment <- getOption('shiny.launch.browser') |>
shinyViewerType attributes() |> unlist() |> unname()
::cli_alert_info("App running in {environment}")
cli::cli_alert_info("shinyViewerType set to {shinyViewerType}")
clielse{
}<- "RStudio"
environment ::cli_alert_info("App not running in {environment}")
cli
}
}
# mod_var_input_ui ----
<- function(id) {
mod_var_input_ui <- NS(id)
ns tagList(
selectInput(
inputId = ns("y"),
label = "Y-axis:",
choices = c("IMDB rating" = "imdb_rating",
"IMDB number of votes" = "imdb_num_votes",
"Critics Score" = "critics_score",
"Audience Score" = "audience_score",
"Runtime" = "runtime"), selected = "audience_score"
),selectInput(inputId = ns("x"),
label = "X-axis:",
choices = c("IMDB rating" = "imdb_rating",
"IMDB number of votes" = "imdb_num_votes",
"Critics Score" = "critics_score",
"Audience Score" = "audience_score",
"Runtime" = "runtime"), selected = "imdb_rating"
),selectInput(inputId = ns("z"),
label = "Color by:",
choices = c("Title Type" = "title_type",
"Genre" = "genre",
"MPAA Rating" = "mpaa_rating",
"Critics Rating" = "critics_rating",
"Audience Rating" = "audience_rating"), selected = "mpaa_rating"
),sliderInput(inputId = ns("alpha"),
label = "Alpha:",
min = 0, max = 1, step = 0.1, value = 0.5
),sliderInput(
inputId = ns("size"),
label = "Size:", min = 0, max = 5, value = 2
),textInput(inputId = ns("plot_title"),
label = "Plot title",
placeholder = "Enter plot title"
)
)
}# test_logger ----
<- function(start = NULL, end = NULL, msg) {
test_logger
if (is.null(start) & is.null(end)) {
cat("\n")
::log_info("{msg}")
logger
else if (!is.null(start) & is.null(end)) {
}
cat("\n")
::log_info("\n[ START {start} = {msg}]")
logger
else if (is.null(start) & !is.null(end)) {
}
cat("\n")
::log_info("\n[ END {end} = {msg}]")
logger
else {
}
cat("\n")
::log_info("\n[ START {start} = {msg}]")
logger
cat("\n")
::log_info("\n[ END {end} = {msg}]")
logger
}
}
We’ll make one small change to style.yaml
before pushing it to GitHub. Instead of having the workflow automatically commit and push the styled code changes to the same branch, we’ll limit the code styling to the 16.1_gha-style
branch:
on:
push:
branches: [16.1_gha-style]
paths: ["**.[rR]", "**.[qrR]md", "**.[rR]markdown", "**.[rR]nw", "**.[rR]profile"]
This change ensures our style
workflow will be triggered only for pushes to the specified branches
(and when changes are made to files with the specified extensions in path
).
After saving these changes to .github/workflows/style.yaml
, we’ll add, commit, and push the changes to GitHub,
git add .
git commit -m "updates to style workflow"
[16.1_gha-style 899bd38] updates to style workflow
4 files changed, 43 insertions(+), 45 deletions(-)
git push
Enumerating objects: 17, done.
Counting objects: 100% (17/17), done.
Delta compression using up to 12 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (9/9), 950 bytes | 25.00 KiB/s, done.
Total 9 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), completed with 6 local objects.
To https://github.com/mjfrigaard/sap.git
d366e0f..899bd38 16.1_gha-style -> 16.1_gha-style
If we view the Actions tab, we’ll see the workflow listed with the name of our commit message:
Clicking on the workflow reveals the following:
The style widget contains each step in the workflow file. If we click on it we can see each step executed in real-time. Below we can see the dependencies installed from the pak
lockfile:
The actual styling comes is in the Style step:
When the workflow has finished, we can pull the style changes to our local branch and view the styled files:5
git pull
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Total 4 (delta 3), reused 4 (delta 3), pack-reused 0
Unpacking objects: 100% (4/4), 450 bytes | 56.00 KiB/s, done.
From https://github.com/mjfrigaard/sap
899bd38..0ad97cf 16.1_gha-style -> origin/16.1_gha-style
Updating 899bd38..0ad97cf
Fast-forward
R/display_type.R | 13 +++----------
R/mod_var_input.R | 30 ++++++++++++++++++++----------
R/test_logger.R | 30 ++++++++++--------------------
3 files changed, 33 insertions(+), 40 deletions(-)
21.2 Shiny app deployment
In this section we’ll be adapting the GitHub Action workflow file provided for deploying a Shiny applications. Before we get to the workflow file, we need to address a few settings and configurations that need to take place outside of the workflow file.
Launch app with the shinypak
package:
launch('16.2_gha-shiny')
21.2.1 Creating repository secrets
We need to make sure our rsconnect
secrets are stored with the GitHub repository (so we can access them from within the workflow file). To do this, we’re going to return to the Settings tab on the sap
repository, expand Secrets and variables under Security, and select Actions:6
Use New repository secret to create three new secrets: RSCONNECT_USER, RSCONNECT_TOKEN, and RSCONNECT_SECRET. You can access these in your shinyapps.io account:
When you’re finished, should see the following three Repository secrets:
21.2.2 Enable private repositories
We also need to make sure our shinyapps.io account allows us to install from private GitHub repositories during a workflow deployment. We can do this in the Profile menu item in our dashboard:
Clicking Update Authentication will open your Posit profile settings, where you can enable private repositories.
21.2.3 Capture dependencies
The documentation for ‘Shiny App Deployment’ indicates we need to create a renv
lockfile in the 16.2_gha-shiny
branch.7
We can create the lockfile with the code below:
install.packages('renv')
::init(force = TRUE) renv
This project contains a DESCRIPTION file.
Which files should renv use for dependency discovery in this project?
1: Use only the DESCRIPTION file. (explicit mode)
2: Use all files in this project. (implicit mode)
We’re going to use the explicit mode
, because the dependencies in the DESCRIPTION
should be all that’s needed to run and deploy the application.
Selection: 1
- Using 'explicit' snapshot type. Please see `?renv::snapshot` for more details.
This project already has a private library. What would you like to do?
1: Activate the project and use the existing library.
2: Re-initialize the project with a new library.
3: Abort project initialization.
We will start fresh and create a new project library:
Selection: 2
- Resolving missing dependencies ...
After renv
has discovered and installed the dependencies, the R session will restart and we’ll see the following new folder/files:
├── .Rprofile
├── renv/
│ ├── activate.R
│ └── settings.json
└── renv.lock
- 1
-
.Rprofile
contains a call tosource("renv/activate.R")
, which manages the project-level dependencies
- 2
-
renv
settings
- 3
-
renv
lockfile
When using renv
in package development, we want to be sure to run renv::install()
and renv::update()
:
::install() renv
- There are no packages to install.
::update() renv
- Checking for updated packages ... Done!
- All packages appear to be up-to-date.
21.2.4 shiny-deploy.yaml
We can include the ‘Shiny App Deployment’ workflow file in sap
with usethis::use_github_action("shiny-deploy")
:8
::use_github_action("shiny-deploy") usethis
✔ Creating '.github/'
✔ Adding '^\\.github$' to '.Rbuildignore'
✔ Adding '*.html' to '.github/.gitignore'
✔ Creating '.github/workflows/'
✔ Saving 'r-lib/actions/examples/shiny-deploy.yaml@v2' to '.github/workflows/shiny-deploy.yaml'
This example workflow file is designed to deploy a Shiny application to a server. We’re going to make a few changes to shiny-deploy.yaml
so it will deploy the application stored in the 16.2_gha-shiny
branch.
The example shiny workflow includes calls to rsconnect::setAccountInfo()
and rsconnect::deployApp()
. We’ll perform a sanity check and confirm we can deploy the application using the information we’ve collected:
Start by entering your rsconnect info:
install.packages('rsconnect') ::setAccountInfo( rsconnectname = "mjfrigaard", token = "YOUR TOKEN", secret = "YOUR SECRET")
- Then try to deploy the application using
rsconnect::deployApp()
:
library(sap) ::deployApp( rsconnectappName = "shinyAppPkgsCICD", account = "mjfrigaard", server = "shinyapps.io", forceUpdate = TRUE)
- Reading the deployment log will tells us if the secret, token, configuration, and lockfile are all working:
- Then try to deploy the application using
── Preparing for deployment ──────────────────────────────────────────────────
✔ Deploying "shinyAppPkgsCICD" using "server: shinyapps.io / username: mjfrigaard"
ℹ Bundling 50 files: .github/workflows/shiny-deploy.yaml, .Rbuildignore, ...,
tests/testthat.R, and vignettes/test-specs.Rmd
ℹ Capturing R dependencies with renv
✔ Found 99 dependencies
✔ Created 2,431,325b bundle
ℹ Uploading bundle...
✔ Uploaded bundle with id 8130675
── Deploying to server ─────────────────────────────────────────────────────────
Waiting for task: 1372980209
building: Processing bundle: 8130675
building: Building image: 9770352
building: Fetching packages
building: Installing packages
building: Installing files
building: Pushing image: 9770352
deploying: Starting instances
unstaging: Stopping old instances
── Deployment complete ─────────────────────────────────────────────────────────
✔ Successfully deployed to <https://mjfrigaard.shinyapps.io/shinyAppPkgsCICD/>
Making sure my application will deploy locally with the code I plan on putting in a workflow file makes me confident it run when it’s triggered. Below we’ll adapt the .github/workflows/shiny-deploy.yaml
file to deploy sap
.
21.2.4.1 Trigger
on:
push:
branches: [16.2_gha-shiny]
name: shiny
- The event we want the workflow triggered
on
is apush
to the16.2_gha-shiny
branch
- We’ll change the
name
toshiny
21.2.4.2 Jobs
jobs:
shiny-deploy:
runs-on: ubuntu-latest
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
jobs
remains unchanged, but the identifier should match thename
. 9
21.2.4.3 Steps
steps:
- uses: actions/checkout@v3
- Check out the repository code
- uses: r-lib/actions/setup-pandoc@v2
- Set up Pandoc for document conversions
- uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true
- Set up R environment
a.use-public-rspm
is the public RStudio package manager
- uses: r-lib/actions/setup-renv@v2
- Set up
renv
to manage the project-specific dependencies captured inrenv.lock
- name: Install rsconnect
run: install.packages("rsconnect")
shell: Rscript {0}
Install rsconnect
the step to deploy our app to the Shiny server
a.run
installs thersconnect
package
In the following steps, we’ll provide our account username (ACCOUNT
) and a name for our app (shinyAppPkgsCICD
). This will deploy an application at https://<ACCOUNT>.shinyapps.io/<APPNAME>/
- name: Authorize and deploy app
env:
APPNAME: shinyAppPkgsCICD
ACCOUNT: mjfrigaard
SERVER: shinyapps.io
Authorize and deploy app
defines the step to deploy our Shiny app
env
sets up the environment variablesAPPNAME
,ACCOUNT
, andSERVER
The final run
step sets the account information using rsconnect::setAccountInfo()
and deploys the app to the specified server using rsconnect::deployApp()
:
run: |
rsconnect::setAccountInfo("${{ secrets.RSCONNECT_USER }}", "${{ secrets.RSCONNECT_TOKEN }}", "${{ secrets.RSCONNECT_SECRET }}")
rsconnect::deployApp(appName = "${{ env.APPNAME }}", account = "${{ env.ACCOUNT }}", server = "${{ env.SERVER }}", forceUpdate = TRUE) shell: Rscript {0}
When we add, commit, and push the changes to the repo, we see the following deployment log:
git add .
git commit -m "shiny deploy workflow"
git push
[16.2_gha-shiny 7953a5c] shiny deploy workflow
10 files changed, 2130 insertions(+)
create mode 100644 .Rprofile
create mode 100644 .github/.gitignore
create mode 100644 .github/workflows/shiny-deploy.yaml
create mode 100644 renv.lock
create mode 100644 renv/.gitignore
create mode 100644 renv/activate.R
create mode 100644 renv/settings.json
create mode 100644 rsconnect/shinyapps.io/mjfrigaard/shinyAppPkgsCICD.dcf
We can see the workflow running on the Actions tab of the sap
repository:
When it’s finished, we can see the deployment log from the workflow looks similar to the log we saw locally:
You can view the deployed application here: https://mjfrigaard.shinyapps.io/shinyAppPkgsCICD/
21.3 Shiny apps & Docker
We covered Docker in the previous chapter, which builds and application in an image and containerizes it’s deployment. In this section we’ll combine Docker and GitHub Actions to automate deployment of our app. Automating app deployments will minimize the risk of environmental discrepancies (avoiding the “it works on my machine” problem) and manual deployment processes. Plus, if something goes wrong after a workflow run, rolling back to a previous version in Git is straightforward.
Launch app with the shinypak
package:
launch('16.3_gha-docker')
21.3.1 Secrets and tokens
Most of the steps below are covered in the excellent blog post by Nicola Rennie10 I’ve updated the contents to work with a Shiny app-package.
To make sure our Connect username, token, and secret travels with our Dockerfile
, we will need to store them as environmental variables.
Environmental variables can be set/unset with Sys.setenv()
/Sys.unsetenv()
:
Sys.setenv(RSCONNECT_USER = '<username>',
RSCONNECT_TOKEN = '<token>',
RSCONNECT_SECRET = '<secret>')
These environmental variables are be passed to rsconnect::setAccountInfo()
with a deploy.R
file.
21.3.2 deploy.R
deploy.R
contains the calls to rsconnect::setAccountInfo()
and rsconnect::deployApp()
we had in our previous workflow file. Sys.getenv()
will retrieve the username, token, and secret stored with Sys.setenv()
:
setAccountInfo(name = Sys.getenv("RSCONNECT_USER"),
token = Sys.getenv("RSCONNECT_TOKEN"),
secret = Sys.getenv("RSCONNECT_SECRET"))
deployApp(appDir = ".",
appName = "shinyAppPkgsDockerCiCd",
account = "mjfrigaard",
server = "shinyapps.io",
forceUpdate = TRUE)
The deploy.R
script should be placed in the root folder of sap
. By default, this will deploy the application launched with app.R
. We’ll change this to the application launched with ggp2_movies_app()
by changing the contents of app.R to the following:
show/hide updated app.R
# set option to turn off loadSupport() ----
::with_options(new = list(shiny.autoload.r = FALSE), code = {
withrif (!interactive()) {
sink(stderr(), type = "output")
tryCatch(
expr = {
library(sap)
},error = function(e) {
::load_all()
pkgload
}
)# create shiny object from dev/ ----
shinyAppDir(appDir =
system.file("dev", package = "sap"))
else {
} ::load_all()
pkgload::ggp2_movies_app(options = list(test.mode = FALSE))
sap
} })
21.3.3 Dockerfile
The Dockerfile
will look similar to the example in the Docker chapter, but with a few important changes:
- The
/home/shinyAppPkgsDockerCiCd
directory is not only the image location we’ll be launching the application from, it’s also the URL for the deployed app:https://
username
.shinyapps.io/shinyAppPkgsDockerCiCd/
- We’ll include
rsconnect
andbslib
in the list of packages to install.
- The final command runs the application from the
deploy.R
file
FROM rocker/shiny
RUN mkdir /home/shinyAppPkgsDockerCiCd
ADD . /home/shinyAppPkgsDockerCiCd
WORKDIR /home/shinyAppPkgsDockerCiCd
RUN R -e 'install.packages(c("rlang", "stringr", "shiny", "ggplot2", "remotes", "rsconnect", "bslib"))'
RUN R -e 'remotes::install_local(upgrade="never")'
EXPOSE 8180
CMD Rscript deploy.R
21.3.4 docker.yml
name: docker-shiny, shinyrpkgs
on:
push:
branches: [ 16.3_gha-docker ]
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t shinyrpkgs .
Add a
name
for the Docker/Shiny workflowSpecify the triggering event the workflow will run
on
The
job
will run onubuntu-latest
(withdocker
id)Checkout the code
Build the docker image with
docker build
The final execute
step is runs docker run and passes our environment variables to the secrets we have stored in GitHub (i.e., with secrets.<SECRET_NAME>
):
- name: execute
run: >
docker run -e RSCONNECT_USER=${{ secrets.RSCONNECT_USER }} -e RSCONNECT_TOKEN=${{ secrets.RSCONNECT_TOKEN }} -e RSCONNECT_SECRET=${{ secrets.RSCONNECT_SECRET }} shinyrpkgs
21.3.5 App dependencies
rsconnect
has a handy appDependencies()
function that returns a data.frame
of packages, versions, and repositories for your application:
head(rsconnect:::appDependencies(appDir = "."))
#> Package Version Source Repository
#> 1 AsioHeaders 1.22.1-2 CRAN https://cran.rstudio.com
#> 2 MASS 7.3-60 CRAN https://cran.rstudio.com
#> 3 Matrix 1.6-4 CRAN https://cran.rstudio.com
#> 4 R6 2.5.1 CRAN https://cran.rstudio.com
#> 5 RColorBrewer 1.1-3 CRAN https://cran.rstudio.com
#> 6 Rcpp 1.0.12 CRAN https://cran.rstudio.com
Passing appDependencies()
in your app-package’s root directory will tell you if any packages have missing Source
or Repository
values (this can cause the deployment to fail).
I also recommend running attachment::att_amend_desc()
to capture all the dependencies in the DESCRIPTION
. For example, att_amend_desc()
added the following packages to the Suggests
field in the DESCRIPTION
:11
[+] 2 package(s) added: shinytest2, testthat.
21.3.6 Docker build
In the docker
workflow log, we can see the commands from our Dockerfile
executed to install the necessary packages:
After installing the dependencies, the sap.tar.gz
is installed from *source*
:
The output above should look familiar–it’s very similar to what we see when we run devtools::load_all()
.
After installing the dependencies, we can see the Shiny deployment log execute in the workflow:
View the deployed application here.
Recap
This chapter will only cover CI/CD with GitHub Actions. Travis-CI has been around longer than GitHub Actions, and if you’d like to compare the two, I recommend this article.↩︎
Triggers can be
push
and/orpull
events to specific branches, creating a release, commenting on an issue, or even configured to run at scheduled times.↩︎If
name
is not provided, GitHub will use the file path↩︎You can find more examples of common GitHub Actions for R packages in r-lib/actions/examples.↩︎
GitHub secrets should be specified by the user following the instructions in the Creating secrets for a repository article.↩︎
“This action assumes you have an
renv
lockfile in your repository that describes the R packages and versions required for your Shiny application.” - Shiny App Deployment↩︎Instructions for the GitHub Action Shiny deployment can be found here↩︎
run-on
is the latest Ubuntu runner andenv
is theGITHUB_PAT
environment variable is your GitHub personal access token (access a secret withsecrets.<SECRET_NAME>
)↩︎Automatically deploying a Shiny app for browsing #RStats tweets with GitHub Actions. Nicola Rennie. October 3, 2022.↩︎
att_amend_desc()
adds adev
folder with a YAML config file, but you can remove this before building/deploying.↩︎
Comments
Lines beginning with
#
will not be executed. It’s common to provide 1) a reference to the workflow source (r-lib/actions
in this case) and 2) a link for help with debugging build failures.