Workflow using the R targets package to import data from the World Ferns list by Michael Hassler and convert it to Darwin Core format.
The pipeline is triggered automatically when a collaborator uploads a new
WorldFerns_ver_*.csv file to a shared private Google Drive folder. A Google
Apps Script watches the folder and fires a GitHub repository dispatch event;
GitHub Actions then downloads the file using a Google service account, runs
the main pipeline, and uploads wf_ppg_genus_plus.csv and
wf_ppg_data_versions.csv as workflow artifacts.
A separate daily check also watches for new
pteridogroup/ppg releases. When one
is found, it bumps the pinned ppg_version in _targets.R and re-triggers
the same import pipeline, which re-downloads the newest World Ferns file from
Drive since no upload event supplied one. See
Automatic PPG version check below.
- Go to Google Drive and click
New → New folder. Name it something like
World Ferns uploads. - Right-click the folder and choose Share. Add your collaborator's email with Editor access so they can upload files to it.
- Ask your collaborator to name their files following the pattern
WorldFerns_ver_YY-MM.csv(e.g.WorldFerns_ver_26-05.csv) so the pipeline can detect them automatically. Files with other names in the same folder are ignored.
The Apps Script bridge needs a token to tell GitHub to start a workflow run.
- Go to github.com/settings/tokens
(you must be logged in to GitHub as an owner or collaborator of
pteridogroup/ppg-import). - Click Generate new token → Generate new token (classic).
- Give it a descriptive name, e.g.
ppg-import Apps Script trigger. - Set the Expiration to something reasonable (90 days or 1 year). You will need to rotate it before it expires.
- Under Select scopes, tick
repo(the full repo scope is needed to firerepository_dispatchevents on a private repo). If the repo is public,public_repois sufficient. - Click Generate token.
- Copy the token immediately — GitHub will only show it once. Paste it somewhere safe (e.g. a password manager) until you add it to Apps Script in step 3d.
Google Apps Script is a free JavaScript scripting environment built into Google Drive. You do not need to install anything — it runs entirely in the browser.
- Open Google Drive and navigate to the folder where your collaborator will upload files.
- Look at the browser URL bar. It will look like:
https://drive.google.com/drive/folders/1A2B3C4D5E6F7G8H9I0J - Copy the long string of letters and numbers at the end — that is your folder ID. Keep it handy for step 3d.
- With the Drive folder open, click New → More → Google Apps Script. (If you don't see Apps Script, click Connect more apps, search for "Apps Script", and install it. Then repeat.)
- A new browser tab opens with the Apps Script editor.
- At the top, click the project name ("Untitled project") and rename it to something like ppg-import trigger.
- In the editor you will see a file called
Code.gswith a default empty function. Select all the existing text and delete it. - Paste the entire block below:
// ── Configuration (values are stored in Script Properties, not here) ── //
const GITHUB_TOKEN = PropertiesService
.getScriptProperties().getProperty("GITHUB_TOKEN");
const GITHUB_REPO = "pteridogroup/ppg-import";
const DRIVE_FOLDER_ID = PropertiesService
.getScriptProperties().getProperty("DRIVE_FOLDER_ID");
// ───────────────────────────────────────────────────────────────────── //
/**
* Run this function ONCE from the editor to install the Drive onChange
* trigger. After that, onDriveChange() fires automatically whenever any
* file is added to your Drive (the function ignores irrelevant files).
* Re-running installDriveTrigger() is safe — it removes any existing
* copy first to avoid duplicates.
*/
function installDriveTrigger() {
// Remove any pre-existing triggers for onDriveChange.
ScriptApp.getProjectTriggers().forEach(function(t) {
if (t.getHandlerFunction() === "onDriveChange") {
ScriptApp.deleteTrigger(t);
}
});
ScriptApp.newTrigger("onDriveChange")
.forDrive()
.onChange()
.create();
Logger.log("Drive onChange trigger installed.");
}
/**
* Called automatically by the Drive onChange trigger whenever a file
* is created, modified, or trashed anywhere in your Drive.
* Filters to WorldFerns_ver_*.csv files in the configured folder,
* then dispatches to GitHub Actions.
*/
function onDriveChange(e) {
// Only react to newly added files.
if (e.changeType !== "ADD") return;
var file;
#### 3c. Paste the script code
1. In the editor you will see a file called `Code.gs` with a default empty
function. **Select all the existing text and delete it.**
2. Paste the entire block below:
```javascript
// ── Configuration (values are stored in Script Properties, not here) ── //
const GITHUB_TOKEN = PropertiesService
.getScriptProperties().getProperty("GITHUB_TOKEN");
const GITHUB_REPO = "pteridogroup/ppg-import";
const DRIVE_FOLDER_ID = PropertiesService
.getScriptProperties().getProperty("DRIVE_FOLDER_ID");
// ───────────────────────────────────────────────────────────────────── //
/**
* Polls the Drive folder for a new WorldFerns file and dispatches to
* GitHub Actions when one is found. Intended to run on a time-driven
* trigger every 15 minutes (see step 3f).
*/
function pollDriveAndDispatch() {
var props = PropertiesService.getScriptProperties();
var folder = DriveApp.getFolderById(DRIVE_FOLDER_ID);
var files = folder.getFilesByType(MimeType.CSV);
// Find the newest WorldFerns_ver_*.csv in the folder.
var newest = null;
while (files.hasNext()) {
var f = files.next();
if (!f.getName().match(/^WorldFerns_ver_.*\.csv$/)) continue;
if (!newest || f.getLastUpdated() > newest.getLastUpdated()) {
newest = f;
}
}
if (!newest) {
Logger.log("No WorldFerns_ver_*.csv found in folder — nothing to do.");
return;
}
// Compare against the last file we dispatched.
var lastId = props.getProperty("LAST_DISPATCHED_FILE_ID");
var lastModified = props.getProperty("LAST_DISPATCHED_MODIFIED");
var currentModified = newest.getLastUpdated().toISOString();
if (newest.getId() === lastId && currentModified === lastModified) {
Logger.log("No new file since last dispatch — skipping.");
return;
}
// Build the GitHub repository_dispatch payload.
var payload = {
event_type: "worldferns_updated",
client_payload: {
file_id: newest.getId(),
file_name: newest.getName(),
modified: currentModified
}
};
var resp = UrlFetchApp.fetch(
"https://api.github.com/repos/" + GITHUB_REPO + "/dispatches",
{
method: "post",
contentType: "application/json",
headers: {
Authorization: "Bearer " + GITHUB_TOKEN,
Accept: "application/vnd.github+json"
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
}
);
if (resp.getResponseCode() >= 300) {
throw new Error("GitHub dispatch failed: " + resp.getContentText());
}
props.setProperty("LAST_DISPATCHED_FILE_ID", newest.getId());
props.setProperty("LAST_DISPATCHED_MODIFIED", currentModified);
Logger.log("Dispatched: " + newest.getName());
}- Click the Save button (disk icon, or Ctrl+S / Cmd+S).
Script Properties are like environment variables — they are stored securely in your Apps Script project and are never visible in the code.
-
In the Apps Script editor, click the gear icon (⚙) in the left sidebar to open Project Settings.
-
Scroll down to the Script properties section and click Add script property.
-
Add the following two properties one at a time:
Property name Value GITHUB_TOKENThe GitHub PAT you created in step 2 DRIVE_FOLDER_IDThe folder ID you copied in step 3a -
Click Save script properties.
The script will also automatically create
LAST_DISPATCHED_FILE_IDandLAST_DISPATCHED_MODIFIEDproperties on its first successful dispatch — you do not need to add those manually.
Before the trigger can fire, you must grant the script permission to access Drive and make external HTTP requests.
- Back in the editor, select
pollDriveAndDispatchin the function dropdown (top toolbar, next to the Run/Debug buttons). - Click Run.
- A dialog box will appear: "Authorization required". Click Review permissions.
- Choose your Google account.
- You may see a warning: "Google hasn't verified this app". Click Advanced → Go to (project name) (unsafe). This is expected for personal scripts — it only means Google has not reviewed the code, not that it is dangerous.
- Click Allow.
- The script will run once. If the folder contains no matching file yet,
you will see
"No WorldFerns_ver_*.csv found in folder — nothing to do."in the Execution log — that is fine.
-
In the left sidebar, click the clock icon (Triggers).
-
Click the + Add Trigger button (bottom right).
-
Fill in the dialog:
Setting Value to select Choose which function to run pollDriveAndDispatchChoose which deployment to run Head Select event source Time-driven Select type of time based trigger Minutes timer Select minute interval Every 15 minutes Failure notification settings Notify me daily (recommended) -
Click Save.
The script will now run every 15 minutes. When a new WorldFerns_ver_*.csv
file is uploaded to the Drive folder, the pipeline will start within
15 minutes.
- Upload a
WorldFerns_ver_*.csvfile to the Drive folder. - Wait up to 15 minutes for the trigger to fire, or run
pollDriveAndDispatchmanually from the editor to trigger immediately. - Go to Apps Script → Executions (clock icon → Executions tab) to
confirm the function ran without errors and logged
"Dispatched: ...". - Go to your GitHub repository → Actions tab. You should see a new workflow run called "Import World Ferns from Google Drive" starting.
- After it completes, click into the run and download the
wf-ppg-genus-plus-csvandwf-ppg-data-versions-csvartifacts.
.github/workflows/check-ppg-version.yml runs daily (and can be triggered
manually) to keep the pinned ppg_version in _targets.R in sync with the
latest pteridogroup/ppg release,
without requiring anyone to notice a new release and hand-edit the file.
- It checks the latest
pteridogroup/ppgrelease tag and compares it against theppg_versionvalue in_targets.R. - If they differ, it updates
_targets.R, commits, and pushes directly tomain. - If the version changed (or the manual
force_runinput is set totrue), it triggersimport-from-drive.ymlwith nofile_id/file_name, so it auto-discovers and downloads the newestWorldFerns_ver_*.csvin the shared Drive folder instead of waiting for an upload event. That run's completion triggersdeploy-shinyapps.ymlautomatically, same as a normal Drive-triggered run.
To force a rebuild and redeploy even when the PPG version hasn't changed (e.g. to pick up a newer World Ferns upload without a matching PPG release), run the workflow manually from the Actions tab, or:
gh workflow run check-ppg-version.yml --repo pteridogroup/ppg-import -f force_run=truecheck-ppg-version.yml itself doesn't talk to Drive directly — it just
dispatches import-from-drive.yml, which (when given no file_id) needs
the GDRIVE_SA_KEY and WF_DRIVE_FOLDER_ID secrets (see the checklist
below) to resolve the latest file itself rather than receiving one from an
upload event.
Set these in GitHub → Settings → Secrets and variables → Actions.
SHINYAPPS_TOKEN(required for deploy workflow)SHINYAPPS_SECRET(required for deploy workflow)GDRIVE_SA_KEY(required for automated imports from the private Drive folder)WF_DRIVE_FOLDER_ID(required for auto-discovery runs ofimport-from-drive.yml— manual dispatch withfile_idleft blank, or triggered bycheck-ppg-version.yml— to look up the newest file in the shared Drive folder; same folder ID as the Apps Script'sDRIVE_FOLDER_IDscript property, step 3a/3d above, just under a distinct name since GitHub Actions secrets and Apps Script script properties are separate namespaces)
SHINYAPPS_ACCOUNTandSHINYAPPS_APP_NAMEare currently hard-coded in.github/workflows/deploy-shinyapps.yml.- The Apps Script's
GITHUB_TOKENscript property (step 3d above) is separate from GitHub Actions secrets — it's a GitHub PAT used by Apps Script to firerepository_dispatchevents, not something Actions itself reads.
The main CI workflow intentionally skips IPNI lookups for speed.
To run IPNI author enrichment manually, use the dedicated targets project:
Sys.setenv(TAR_PROJECT = "ipni")
targets::tar_make(names = wf_dwc_ipni_csv)This writes an IPNI-enriched CSV to:
_targets/user/results/wf_dwc_ipni.csv