GMD Human Review App — How It Works

A plain-language guide for whoever picks this up next. No R, no jargon. By the end you should know what each piece does, who can do what, and how to push a change to the live app.

What's in here
  1. The big picture
  2. The four moving parts
  3. What happens when someone reviews a variable
  4. Who is allowed to do what
  5. How to push a change to the live app
  6. Common tasks & recipes
  7. Troubleshooting quick-reference
  8. Glossary

1. The big picture

The GMD team writes canonical variable rules — the official guidance for how household survey variables should be harmonized across countries. A computer (an AI-assisted draft) proposes how a new survey's variables map to those rules. A human needs to look at each proposal and either accept it, send it back for revision, or block it.

The Human Review App is the website where humans do that. It's a small Shiny web app running on the World Bank's Posit Connect server. Reviewers open it in a browser, see a queue of items to review, and click through to approve, request changes, or reopen previously approved items.

Survey data (raw country file) Draft proposals (AI-assisted) Canonical rules (governed) Reviewer verdict (approve / revise / reopen) Drafts are checked against rules; humans give the final verdict.

Figure 1. Where the app fits: it sits between the drafts and the canonical rules.


2. The four moving parts

Think of the system as four boxes that talk to each other. You can keep the whole thing running just by understanding what each one does.

GitHub repo Stores: • app source code • variable drafts • review records Posit Connect Hosts: • the live web app • secrets (env vars) • user identity Shiny app • the dashboard • reads/writes via GitHub API • enforces roles Browser Reviewer / approver / admin opens the URL, sees queue, makes a decision. Connect tells the app who they are (their role follows).

Figure 2. The four boxes and the path data takes when a reviewer opens the app.

1. GitHub repo

Holds all the source code, the variable drafts being reviewed, and the review records themselves. Branches matter: main holds drafts; the protected review branch holds review records.

2. Posit Connect

The internal server that hosts the live web app. It also holds the secret credentials the app needs (e.g. the GitHub App key). Click Deploy on a content item to re-run the existing code — but to push new code you must use rsconnect::deployApp().

3. Shiny app

The web UI itself. Reads drafts from main, writes review records to review, enforces who can approve what. Talks to GitHub over the API using a GitHub App installation.

4. Browser / human

Whoever is reviewing. Connect tells the app their username; the app looks up their role in config/roles.yml and shows or hides the buttons they are allowed to click.

Important mental model: the GitHub repo is the source of truth for both the code and the data being reviewed. A deployment is just "send the latest code to the server and run it". A change to who-can-do-what is just "edit config/roles.yml, commit, redeploy".

3. What happens when someone reviews a variable

The app enforces a small state machine. A variable moves through these states; only certain roles can move it forward.

draft (in main branch) in-review needs-revision approved → review record written (to the review branch) submit (reviewer) request-revision (approver) approve (approver) submit (reviewer) reopen (admin) Every state change is appended to an immutable audit trail.

Figure 3. The lifecycle of a single review item, with the role allowed for each transition.

Plain-English version of the same flow:

  1. A draft appears in the queue.
  2. A reviewer reads it and clicks Submit when they think it's ready.
  3. An approver either clicks Approve (it goes to the canonical output) or Request revision (it goes back).
  4. If it's been approved, only an administrator can reopen it.
  5. If it was sent back for revision, a reviewer can submit again (the review round counter increments).

4. Who is allowed to do what

There are exactly three roles, defined in review-app/config/roles.yml. Each person is mapped to exactly one role (their username must match what Connect returns in session$user).

RoleCan doCannot do
reviewer Save / edit a draft body. Submit a draft into review. Approve or send back. Reopen anything.
approver Approve an item under review. Send it back for revision. Edit the body. Reopen approved items.
administrator Reopen an already-approved item. Assign work. (Implicitly: everything the others can do if they were also mapped.) —

Current map: 1 administrator (acastanedaa), 3 approvers (clakner, dmahler, mnguyen3), 15 reviewers. Edit review-app/config/roles.yml, commit, push, and redeploy to change it. There is no in-app way to edit roles — that's a deliberate safety choice.

How the role is picked at runtime. When a reviewer opens the URL, Connect tells the app their username. The app reads config/roles.yml from its own bundle and looks that username up. If the username is missing, the app treats the visitor as having no role — they can browse but can't take any action. Never a default role.

5. How to push a change to the live app

This is the most important section for whoever takes over. Two facts that often surprise people:

  1. The "Deploy" button in the Connect UI does NOT pull new code from GitHub. It just re-runs the bundle that's already uploaded. To get new code live, you must push a fresh bundle from your machine.
  2. Pushing to main does NOT auto-deploy. GitHub is the source of truth, but Connect doesn't watch it. You still have to run the deploy command below.
Mental model: "git push" saves your work. "deployApp()" ships it. Two steps.

The deploy command (run from the repo root)

Every code or role change goes out with this recipe. It works on macOS or Linux, from any terminal that has R installed.

# 1. Make sure your working copy is clean and on main
cd /path/to/GMD-canonical-schema
git checkout main
git pull

# 2. Make your changes (edit code, edit roles.yml, etc.)

# 3. Commit and push
git add .
git commit -m "describe what you changed"
git push origin main

# 4. Ship to Connect (this is the magic command)
RENV_CONFIG_AUTOLOADER_ENABLED=FALSE Rscript -e '
rsconnect::deployApp(
  appDir      = "review-app",
  appFiles    = c("DESCRIPTION", "NAMESPACE", "app.R", "renv.lock",
                  "R/", "inst/", "config/"),
  appName     = "review-app",
  appTitle    = "review-app",
  appId       = "88dcd5b3-91b0-47fd-97b6-b1e420c683f2",
  account     = "acastanedaa",
  server      = "wbconnect",
  lint        = FALSE,
  dependencyResolution = "library"
)'

What those flags mean

Pre-requisites (one-time setup)

Treat this section like a secret. The API key is a credential. Never commit it; never paste it in chat; never email it unencrypted.

Before any of the above works, the machine you're deploying from needs three things in ~/.Renviron:

CONNECT_SERVER=https://datanalytics-int.worldbank.org
CONNECT_API_KEY=your-personal-key-here

The API key comes from the Connect UI (your name → API Keys → Add). The first deploy also requires the rsconnect R package, installed once with:

Rscript -e 'install.packages("rsconnect")'

On macOS this writes to ~/Library/R/<arch>/<R-version>/library and works for every subsequent deploy.


6. Common tasks & recipes

Add or remove a reviewer

  1. Edit review-app/config/roles.yml — add or remove the - identity: entry.
  2. Commit, push, redeploy (see section 5).

Promote someone from reviewer to approver

Same as above — change their role: line from reviewer to approver, commit, push, redeploy.

Change a code bug or feature

  1. Edit the relevant R/<file>.R.
  2. Commit, push, redeploy.

Rotate the GitHub App private key (compromise / annual rotation)

  1. Generate a new private key in GitHub → your GitHub App → Private keys → Generate.
  2. Save the new .pem to a secure location.
  3. Set the new key on the content item. From the repo root:
    Rscript -e '
    suppressMessages(library(httr))
    pem <- paste(readLines("/secure/path/new-key.pem"), collapse = "\n")
    httr::PUT(paste0(Sys.getenv("CONNECT_SERVER"),
                     "/__api__/v1/content/88dcd5b3-91b0-47fd-97b6-b1e420c683f2/environment"),
      httr::add_headers(Authorization = paste0("Key ", Sys.getenv("CONNECT_API_KEY")),
                        "Content-Type" = "application/json"),
      body = jsonlite::toJSON(list(list(name="GITHUB_APP_PRIVATE_KEY", value=pem)),
                              auto_unbox=TRUE))
    '
  4. The next deploy picks up the new secret automatically (env vars are read at app startup).

Add a new env var (e.g. another GitHub App)

Same PUT shape as above; send an array of {name, value} objects.

Roll back a bad deploy

  1. In Connect, find the previous content version (or previous bundle ID) and redeploy from the UI.
  2. OR: git revert the offending commit, push, and run the deploy command again.

7. Troubleshooting quick-reference

SymptomLikely causeFix
App shows "no package called 'reviewapp'" Old deployment still using library(reviewapp) instead of pkgload::load_all(). Re-run the deploy command. The current app.R uses pkgload::load_all(".") + run_app().
App shows "review app adapter not configured" One or more of the 7 GitHub env vars are missing. Use the PUT to /environment above to set REVIEW_APP_GH_* and GITHUB_APP_* on the content item, then redeploy.
Visitor sees "Authenticated as ... not authorized" Their Connect username is not in config/roles.yml. Add them to roles.yml with the right role, commit, push, redeploy.
Connect UI "Deploy" does nothing visible That's by design — it re-runs the same bundle, so nothing changes. Use the rsconnect::deployApp() command to push a new bundle.
Deploy command fails with "Library and lockfile out of sync" rsconnect wants to verify renv.lock against a local library we don't use. The command already passes dependencyResolution = "library" — that flag must be present.
Deploy command fails with "packages installed from unknown source: reviewapp" An old reviewapp installation sits in your user library. Run Rscript -e 'remove.packages("reviewapp")' once, then retry.

8. Glossary

TermPlain meaning
BundleA .tar.gz the server unpacks to run the app. Contains code + config.
ConnectThe internal server that hosts the web app.
DeploySend a fresh bundle to the server and (re)start the app.
GitHub AppA "bot identity" the app uses to read drafts and write review records to GitHub without using anyone's personal token.
GolemAn R package framework for building Shiny apps as proper R packages. The review-app is a Golem app.
pkgload::load_allAn R function that loads an R package straight from source code without installing it. The app uses this so Connect doesn't need to install the local package.
renv.lockA snapshot of every R package version the app needs. Connect reads this to install the right versions server-side.
rsconnectThe R package that talks to Posit Connect — registers servers, authenticates, uploads bundles.
ShinyAn R framework for interactive web apps.