-
Notifications
You must be signed in to change notification settings - Fork 3
/
shiny-ci-cd.Rmd
238 lines (164 loc) · 11.6 KB
/
shiny-ci-cd.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# CI/CD pipelines for automatic deployment of a R Shiny web app
```{r setup-shiny-ci-cd, include=FALSE}
knitr::opts_chunk$set(echo = TRUE, collapse = TRUE, eval = FALSE)
```
It is good practice to integrate and develop an R Shiny app as an R package, to take full advantage of all the integrated features established for R packages (e.g., documentation, package namespaces, automated testing, `R CMD check`, etc.). A typical development workflow to package a Shiny app is provided by the [`golem` package](https://cran.r-project.org/web/packages/golem/index.html). Later in this chapter we will also indicate how to package a shiny app without the infrastructure provided by `golem`.
Furthermore, version control systems such as Git are a great asset for keeping track an manage changes, especially in a collaborative setup.
The development of a packaged Shiny app under version control can easily enable and take advantage of:
- Continuous Integration (CI) pipelines to automate checks and ensure higher code quality and robustness;
- Continuous Deployment (CD) pipelines to automate the process of deployment to a _productive_ environment.
This guide illustrates how to set up CI/CD pipelines on the popular free and open source services [Travis CI](https://travis-ci.com) and [GitHub Actions](https://github.com/features/actions) for a packaged Shiny app on a GitHub repository, deployed and hosted on [shinyapps.io](https://www.shinyapps.io).
As a side note, [Travis CI](https://travis-ci.com)'s change in policy does not make it advantageous for open source projects any longer, as they usually rely on free plans. We therefore suggest to use [GitHub Actions](https://github.com/features/actions) for open source projects.
[ShinyCICD](https://github.com/miraisolutions/ShinyCICD) is a minimal example of a packaged Shiny app that will be used as an example throughout the guide. You can simply [fork](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo) the repository and setup your specific user settings (especially for shinyapps.io) to see CI/CD pipelines in actions, or follow the steps described below to setup CI/CD pipelines for your own app.
## Generic CI/CD pipeline
Generally speaking, a CI/CD pipeline related to an R package is comprised of the following steps:
- setup a running environment
- setup R
- check out the package source code
- install system dependencies
- install package dependencies (with caching)
- build the package
- checks the package
- deploy
Most of these steps are implemented by default in Travis CI for an R package. In GitHub Actions, on the other hand, it is currently necessary to manually specify each of them.
## Travis CI
Travis CI is an open-source continuous integration service that can be used to build and test software projects hosted on GitHub. To set up Travis CI you need to login at [https://travis-ci.com/](https://travis-ci.com/) (using your GitHub account) and provide authorization via GitHub (see [Travis CI Tutorial](https://docs.travis-ci.com/user/tutorial)).
### Standard CI setup
To setup Travis CI in a project use:
```{r, eval = F}
usethis::use_travis() # use ext = "com" if usethis < 1.6.0
```
This will generate a generic `.travis.yml` file
```yaml
# R for travis: see documentation at https://docs.travis-ci.com/user/languages/r
language: R
cache: packages
```
As default, Travis CI takes care of package dependency installation and performs the typical package build & check you would run locally via e.g. `devtools::check()`. Such CI pipeline is triggered by any push event on any branch on the GitHub repo, including pull requests.
![](shiny-ci-cd/img/ShinyCICD_travis1.png)
### Using renv for your project
If your project relies on package [renv](https://rstudio.github.io/renv) for tracking dependencies via an `renv.lock` file, you should override the default `install`ation package dependencies and make sure `cache`ing is adjusted accordingly, as follows
```yaml
cache:
directories:
- $HOME/.local/share/renv
- $TRAVIS_BUILD_DIR/renv/library
install:
- Rscript -e "if (!requireNamespace('renv', quietly = TRUE)) install.packages('renv')"
- Rscript -e "renv::restore()"
```
as described in the [Using renv with Continuous Integration](https://rstudio.github.io/renv/articles/ci.html
) vignette.
### Automated deployment
Travis CI can be setup to perform a deployment (e.g. publish a shiny app on [shinyapps.io](https://www.shinyapps.io/)) upon any push to the `master` branch, provided the CI checks pass.
This is achieved for a shinyapps.io deployment by specifying in `.travis.yml` an additional `deploy:` section as
```yaml
deploy:
provider: script
skip_cleanup: true # strictly necessary only for the renv case
script:
- >-
Rscript
-e 'account_info <- lapply(paste0("SHINYAPPS_", c("ACCOUNT", "TOKEN", "SECRET")), Sys.getenv)'
-e 'do.call(rsconnect::setAccountInfo, account_info)'
-e 'rsconnect::deployApp(appName = "ShinyCICD")'
on:
branch: master
```
where `SHINYAPPS_ACCOUNT`, `SHINYAPPS_TOKEN`, `SHINYAPPS_SECRET` are [secure variables defined on Travis CI](https://docs.travis-ci.com/user/environment-variables/) holding your account name and corresponding [tokens](https://docs.rstudio.com/shinyapps.io/getting-started.html#deploying-applications) for shinyapps.io.
It is in fact more convenient to write an R script, saved e.g. as `deploy/deploy-shinyapps.R` (build-ignored via `usethis::use_build_ignore("deploy")`) defining the deployment commands:
```{r read-deploy-script, eval = TRUE, echo = FALSE}
knitr::read_chunk("shiny-ci-cd/deploy/deploy-shinyapps.R", labels = "deploy-shinyapps")
```
```{r deploy-shinyapps}
```
and then simply execute it as `deploy` `script`:
```yml
deploy:
provider: script
skip_cleanup: true # strictly necessary only for the renv case
script: Rscript deploy/deploy-shinyapps.R
on:
branch: master
```
### Putting it all together
The final `.travis.yml` file (for the non-renv case) would look like
```yaml
# R for travis: see documentation at https://docs.travis-ci.com/user/languages/r
language: R
cache: packages
deploy:
provider: script
script: Rscript deploy/deploy-shinyapps.R
on:
branch: master
```
As visible from the run logs, all the CI/CD pipeline steps are performed, despite only the deployment step being explicitly defined.
![](shiny-ci-cd/img/ShinyCICD_travis2.png)
## GitHub Actions
[GitHub Actions](https://docs.github.com/en/free-pro-team@latest/actions) is a service for running highly-customizable and flexible automated workflows, fully integrated with GitHub and very suitable to CI/CD pipelines.
[Workflows](https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions) 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.
### Workflow steps
A workflow should have an identifying `name` and an `on` section indicating 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 a (predefined) action or a script, however, for the time being, GitHub Actions does not support a hierarchical, aggregated structure of actions. Being fully customizable, it is necessary to fully specify each step in the CI/CD pipeline explicitly.
#### Setup
- Checkout the source package from the repo, using `actions/checkout` provided by GitHub.
- Setup R using action [`r-lib/actions/setup-r`](https://github.com/r-lib/actions#readme)
- Query and cache R package dependencies using `remotes::dev_package_deps()` and the `actions/cache` predefined action.
- Install system dependencies using package `sysreqs::sysreq_commands()` (for the `ubuntu` runner used to run the action)
- Install R package dependencies using `remotes::Install_deps()`
#### Package check
- Check the package via using `rcmdcheck::rcmdcheck()`
#### Deployment
- Deploy to shinyapps.io, similar to the Travis CI approach:
- In this case, environment variables `SHINYAPPS_ACCOUNT`, `SHINYAPPS_TOKEN` and `SHINYAPPS_SECRET`, defining credentials for [shinyapps.io](https://www.shinyapps.io/)), are specified / accessible as GitHub [secrets](https://docs.github.com/en/free-pro-team@latest/actions/reference#authentication-and-secrets).
- An 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:
```{r deploy-shinyapps}
```
### Workflow file
The `steps` above are defined in the `.yml` workflow file as follows
```{r read-ci-cd.yml, eval = TRUE, include = FALSE}
file.copy("shiny-ci-cd", "_book", recursive = TRUE)
knitr::read_chunk("shiny-ci-cd/actions/ci-cd.yml", labels = "ci-cd.yml")
```
```{yml ci-cd.yml}
```
As visible from the run logs, all the CI/CD pipeline steps are performed subsequently, and are identifiable by the `name` field.
![](shiny-ci-cd/img/ShinyCICD_githubactions1.png)
### `usethis::use_github_action()`
In order to use the GitHub action workflow above, or its renv-based variant, you can simply:
```{r}
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"
```
## Deploying a packaged shiny application
It makes sense to structure shiny applications as a package to better control their dependencies. However, some structural conditions are required for the deployment of a packaged shiny application.
As already mentioned, one option is to use the [`golem` package](https://cran.r-project.org/web/packages/golem/index.html), which will initialize the shiny application with its framework that does support deployment of a shiny application as a package. But sometimes you may not want to add an entire framework to an existing application and instead add this support manually.
Since we did not find any good documentation of this online (as of Nov 2020), we investigated this ourselves and are happy to share our findings here.
### Entry point
The application needs an entry point which should be named `app.R` and be situated in the root of the package, i.e. where
`DESCRIPTION` and `NAMESPACE` are located.
It should contain only what is required for the entry point to be able to load the application, e.g.:
```{r, eval = F}
pkgload::load_all(export_all = FALSE, helpers = FALSE, attach_testthat = FALSE)
# PKG is the name of the packaged shiny application
# run_PKG_app is a function that wraps around shiny::shinyApp()
PKG::run_PKG_app()
```
```{r, eval = F}
run_PKG_app <- function() {
shinyApp(ui = ui, server = server)
}
# where ui and server are both functions
```
### server and ui
Both `server` and `ui` need to be functions in order to work in the packaged shiny application context. `server` should already be a function and it is enough to wrap `ui` into a function without any arguments or return statements.
In the beginning of the `ui` function, we also need to add a call to `shiny::addResourcePath` to make static resources available.
### non-CRAN dependencies
Deploying a packaged shiny application which uses non-CRAN sources like Github can also cause issues. It is recommended to list these dependencies under [`Remotes:`](https://cran.r-project.org/web/packages/devtools/vignettes/dependencies.html) instead of e.g. `Imports:`, to make sure package versioning tools like `renv` notice the difference.