Using Git in Shiny Apps

August 24, 2023

This is a brief blog post to document how you can use Git in a Shiny app.

In August 2023, Rik and me were revising our “Knowing What We’re Talking About: Facilitating Decentralized, Unequivocal Publication of and Reference to Psychological Construct Definitions and Instructions” article (preprinted here)https://doi.org/jnjp. If you find the topic interesting, I have a brief blog post here and the same in a Mastodon thread here - but this post is about one part of Constructor, the Shiny app we created to accompany this article.

Constructor allows people to import or create a DCT specification. The app allows people to edit the DCT specification, shows a human-readable overview, and produces the YAML code used to store DCT specifications. Finally, it allows people to upload their DCT specification to the PsyCoRe.one repository.

These uploads work with a git repository: the PsyCoRe.one repository is a Hugo website, and the overview table with constructs, as well as the other pages (e.g. each construct’s overview page) are produced by Hugo as it loops over a series of YAML files (that hold each DCT specification). The rebuilding of this Hugo site is triggered by a webhook called by the Git forge (currently GitLab, but we may migrate to Codeberg at some point).

For all this magic to work, Constructor has to commit each newly added DCT specification to the Git repository holding the PsyCoRe.one website. For another project I’m involved in and that uses almost the same back-end infrastructure, OpenDrawer, James Green and Cillian McHugh added such Git functionality. So, I respectfully, um, borrowed ir for Constructor. However, this broke down at some point (for both Shiny apps). In the process of fixing it, I ended up switching to {gert}. However, even there I didn’t get it to work. After trying for an hour or two, I decided to ask on Mastodon whether maybe somebody had solved this already (see this post). Gabor Csardi, one of the amazing developers at Posit, responded. And finally, after about four more hours, I got it working.

I wrote this brief blog post to explain how. The thing that was going wrong turned out to be that I set the Personal Access Token (PAT) as an environment variable using:

Sys.setenv(PSYCORE_PSYCONSTRUCTOR_GITLAB_PAT = "token");

(But then with token replaced with the token.)

However, it turned out (after those four more hours of troubleshooting) that Shiny doesn’t succesfully bundle this environment variable when it pushes the image of the Shiny app to the ShinyApps.io server…

I found out by throwing a message, warning, and error in the Shiny app:

message(paste0("PAT:[", Sys.getenv("PSYCORE_PSYCONSTRUCTOR_GITLAB_PAT"), "]"));
warning(paste0("PAT:[", Sys.getenv("PSYCORE_PSYCONSTRUCTOR_GITLAB_PAT"), "]"));
stop(paste0("PAT:[", Sys.getenv("PSYCORE_PSYCONSTRUCTOR_GITLAB_PAT"), "]"));

This showed me in the logs that that environment variable was empty.

Now I use .Renviron, located in the project directory, and added to the .gitignore file since it contains the token. Its contents are simply:

PSYCORE_PSYCONSTRUCTOR_GITLAB_PAT=token

(Again, with token replaced with the token.)

For a bit more information about this, see this Posit post as well as this reply by Hadley to a Stack Overflow question.

The full code fragment (for the entire application, see this Codeberg repo) for cloning a git repo, adding a file, staging it, committing it, and pushing to the remote git repo is:

###-------------------------------------------------------------------------
### Git repo and credential configuration
###-------------------------------------------------------------------------

userName <- "PsyConstructor";
userMail <- "psyconstructor@psycore.one";
gitForge <- "https://gitlab.com";
repoUrl <- paste0(gitForge, "/psy-ops/psycore-one.git");

###-------------------------------------------------------------------------
### Clone git repo
###-------------------------------------------------------------------------

tempPath <- tempfile(pattern = "gertTempRepo-");

tempRepo <- gert::git_clone(
  repoUrl,
  path = tempPath
);

### I know it's weird and seems ill-advised, but without this, it doesn't work.
setwd(tempPath);

### New approach: project should create a .gitignored file called
### ".Renviron" and which should contain a row:
### PSYCORE_PSYCONSTRUCTOR_GITLAB_PAT=token
### where "token" is the token. Note that upon a new clone, you will
### have to create this file again.

### So _don't_ set environment variables with `Sys.setenv()`!
# Sys.getenv("PSYCORE_PSYCONSTRUCTOR_GITLAB_PAT");
# Sys.setenv(PSYCORE_PSYCONSTRUCTOR_GITLAB_PAT = "token");
# Sys.getenv("PSYCORE_CONSTRUCTOR_CODEBERG_PAT");
# Sys.setenv(PSYCORE_CONSTRUCTOR_CODEBERG_PAT = "token");

### https://fosstodon.org/@gaborcsardi/110940818918930959

system(paste0("git config --global credential.helper cache"));
system(paste0("git config --global credential.username ", userName));
system(paste0("git config --local user.name ", userName));
system(paste0("git config --local user.email ", userMail));
gitcreds::gitcreds_approve(list(
  url = "https://gitlab.com",
  #url = "https://codeberg.org",
  username = userName,
  #password = Sys.getenv("PSYCORE_CONSTRUCTOR_CODEBERG_PAT")
  password = Sys.getenv("PSYCORE_PSYCONSTRUCTOR_GITLAB_PAT")
))

###-------------------------------------------------------------------------
### Write, stage, and commit file, and push to repo
###-------------------------------------------------------------------------

### {gert} wants the path relative to the repository root
tempFile <-
  file.path("data", "dctspecs", paste0(ucid(), ".dct.yaml"));

psyverse::save_to_yaml(
  dctSpec(),
  file = file.path(tempPath, tempFile)
);

gert::git_add(
  files = tempFile,
  repo = tempRepo
);

gert::git_commit(
  paste("Submission of DCT specification with UCID ", ucid()),
  repo = tempRepo
);

gert::git_push(
  repo = tempRepo
);

In my view it’s superweird that the working directory has to be set, but when I remove it, it stops working… That might a a {gert} thing?

In any case, I hope this post can help potential future people who run into the same problem. And otherwise at least James Green and Cillian McHugh, can use it for OpenDrawer. Or future me 😬