Central, reusable GitHub Actions for d-morrison / UCD-SERG / ucdavis R-package
and Quarto repositories. Modeled on
r-lib/actions and
easystats/workflows: repos call a
reusable workflow with a tiny stub instead of carrying their own copy.
This repo is public so it can be referenced from repositories across the
d-morrison, ucdavis, UCD-SERG, UCLA-PHP, and UCD-IDDRC owners.
Each capability is shipped as two layers:
- Composite action (e.g.
check-bibliography-dois/action.yml) — bundles the real steps and any helper script. Referenced asd-morrison/gha/<name>@v1. - Reusable workflow (
.github/workflows/<name>.yml,on: workflow_call) — wraps the composite, declares permissions, and checks out the caller's repo. This is what consumer repos target.
A consumer repo adds a small caller stub (see examples/):
name: Check Bibliography DOIs
on:
push: { branches: [main] }
pull_request:
workflow_dispatch:
jobs:
check:
uses: d-morrison/gha/.github/workflows/check-bibliography-dois.yml@v1Pin to @v1 (a moving major tag updated as fixes land). Do not reference
@main from consumers.
| Workflow | Purpose | Key inputs |
|---|---|---|
check-bibliography-dois.yml |
Validate book/article BibTeX entries have resolvable DOIs matching CrossRef metadata | exclude-keys, install-quarto, no-metadata-check |
check-non-standard-chars.yml |
Detect curly quotes / en–em dashes in .qmd and .R files |
python-version |
check-phi.yml |
Scan PRs (added lines only) for content that looks like PHI — SSNs, medical record numbers, dates of birth, PHI column headers in data files | detectors, paths-ignore, allowlist-file, fail |
check-links.yml |
lychee link check with bundled config, PR skip-label, and auto-issue on main |
lychee-config, lychee-args, create-issue-on-main, skip-label |
summary.yml |
AI summary comment on newly opened issues | — |
check-news.yml |
Enforce a NEWS.md changelog entry on PRs (wraps UCD-SERG/changelog-check-action) |
changelog |
claude.yml |
Agent-mode Claude Code bot: responds to @claude mentions, edits files, opens/updates PRs |
setup-r, install-quarto, use-renv, apt-packages, pip-packages, checkout-submodules, link-skills, eager-pr, prompt-addendum, webfetch-allowlist-url, reviewer |
claude-code-review.yml |
Read-only Claude PR review (runs the code-review plugin; inline findings on pull_request runs, consolidated summary on dispatched runs) |
pr-number, prompt-addendum, checkout-submodules |
quarto-publish.yml |
Render a Quarto site and deploy it to GitHub Pages | path, setup-r, r-packages, use-renv, tinytex, apt-packages, output-dir, checkout-submodules, pre-render-artifact, pre-render-artifact-path, deploy |
preview.yml |
Build half of the PR-preview family: render a Quarto site in the (possibly fork) PR context and upload it + PR metadata as an artifact (read-only) | r-version, apt-packages, use-renv, install-package, setup-chrome, submodules, render-profile |
preview-deploy.yml |
Deploy half: on workflow_run completion of the build, publish the artifact to gh-pages and comment the preview link (base-repo context) |
— |
cleanup-pr-previews.yml |
Housekeeping: delete gh-pages preview directories for PRs that are no longer open |
preview-dir |
bump-submodule.yml |
Update a named submodule to its upstream HEAD and open a PR when the pointer moves | submodule-path, remote-branch, base-branch, pr-branch |
sync-shared-fragments.yml |
Vendor files from an upstream repo (pinned to a commit, recorded in a manifest) and open a PR when they change — avoids a recursive mutual submodule | source-repo, source-ref, source-paths, dest-dir, manifest-path |
A called reusable workflow cannot hold more GITHUB_TOKEN permissions than the
caller grants, and most repos default to a read-only token. So workflows
that need to write must have the caller grant it on the calling job:
check-links(opens an issue onmainfailures) → grantissues: write,pull-requests: read,contents: read.summary(comments on issues, calls the models API) → grantissues: write,models: read,contents: read.check-bibliography-dois,check-non-standard-chars,check-phi→ onlycontents: read(the default), so nopermissions:block is needed.quarto-publish(deploys to GitHub Pages) → grantpages: write,id-token: write,contents: read, and set Settings → Pages → Source = "GitHub Actions" once. Build-only callers (deploy: false) need justcontents: read.claude(pushes branches, opens PRs, dispatches the review workflow) → grantcontents: write,pull-requests: write,issues: write,id-token: write,actions: write, and add theCLAUDE_CODE_OAUTH_TOKENsecret.- Optional: if Claude will edit files under
.github/workflows/, also add aWORKFLOW_TOKENsecret (a PAT or GitHub App token withcontents:write+workflows:write). The integratedGITHUB_TOKENcannot push workflow-file changes — GitHub rejects them without theworkflowsscope. Repos that never touch.github/workflows/can omit it; pushes fall back toGITHUB_TOKEN. Note that, unlikeGITHUB_TOKEN, a PAT/App-token push does trigger otherpush-based workflows, so enablingWORKFLOW_TOKENcan set off extra CI runs. - Optional: set
checkout-submodules: trueso Claude can read submodule contents. Public submodules clone anonymously; private ones additionally need aSUBMODULES_TOKENsecret.
- Optional: if Claude will edit files under
claude-code-review(read-only review) → grantcontents: read,pull-requests: write,issues: write,id-token: write, and theCLAUDE_CODE_OAUTH_TOKENsecret.- Optional: set
checkout-submodules: trueso the reviewer can read submodule contents instead of reporting them as uninitialized. Public submodules clone anonymously; private ones additionally need aSUBMODULES_TOKENsecret.
- Optional: set
preview(build half, read-only) → onlycontents: read(the default).preview-deploy(deploy half, pushesgh-pages+ comments) → grantcontents: write,pull-requests: write,actions: read.cleanup-pr-previews(commits deletions togh-pages) → grantcontents: write,pull-requests: read.bump-submodule,sync-shared-fragments(open a PR) → grantcontents: write,pull-requests: write, and enable Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests" so the integratedGITHUB_TOKENcan open the PR. For private submodules,bump-submodulealso needs aSUBMODULES_TOKENsecret. Add aWORKFLOW_TOKENonly to push to a protected branch; otherwise pushes fall back toGITHUB_TOKEN.
The stubs in examples/ already include the right permissions:
blocks — copy them as-is.
The two Claude workflows are a pair: an @claude review mention (or any commit
Claude pushes) routes through claude.yml, which dispatches claude-code-review.yml
via workflow_dispatch. Install both, and keep the review stub named
claude-code-review.yml (or set claude.yml's review-workflow-file input to
match) so the dispatch resolves.
check-phi is a heuristic tripwire, not a HIPAA compliance tool. It flags
patterns that should almost never be committed — US Social Security numbers,
medical record numbers, dates of birth, and PHI-suggestive column headers in
delimited data files (.csv/.tsv/.psv) — so a human reviews before the
data merges. It is tuned for high precision (few false positives), so it will
miss free-text PHI such as patient names. The phone and email detectors
exist but are off by default (too noisy in source); enable them via the
detectors input.
- Diff-scoped on PRs. Only lines added by the PR are scanned, so existing
fixtures don't re-trip the check on unrelated edits.
pushruns scan the whole tracked tree (git ls-files). - Values are never printed. A leaked identifier in a CI log is still a leak,
so findings report only
file:line:coland the detector name — never the matched text. Findings appear as inline annotations on the PR. - Suppressing false positives (e.g. synthetic test data): add a
phi-allowcomment on the line, or list a regex matching the value in an allowlist file (defaults to.github/phi-allowlist.txtwhen present; override with theallowlist-fileinput). Usefail: falseto downgrade to warnings.
The PR-preview family publishes a rendered Quarto site for each open PR to a
pr-preview/pr-<n>/ directory on gh-pages. It is three cooperating
workflows — install all three stubs from examples/:
preview.yml(build) — triggered onpull_request. Renders the site and uploads it plus the PR metadata as apr-preview-siteartifact. Runs read-only in the (possibly fork) PR context, so it can't write to the base repo.preview-deploy.yml(deploy) — triggered onworkflow_runcompletion of the build. Downloads the artifact and publishes it togh-pagesin the base-repo context (where the token can write), then comments the preview link on the PR.cleanup-pr-previews.yml(housekeeping) — scheduled. Removes preview directories for PRs that have closed.
The build/deploy split is a trust boundary: untrusted fork code only ever
runs in the read-only build half, while the privileged gh-pages push happens
in the deploy half against base-repo code. Don't collapse them into one job.
Two wiring requirements:
- The deploy stub's
on: workflow_run: workflows:value must match the build stub'sname:(both default toQuarto Preview Buildin the examples). That string is howworkflow_runfinds the build. workflow_runandscheduletriggers only fire for the copy of the file on the default branch, so previews and cleanup don't take effect until the stubs are merged tomain.
The build half is parameterized for non-rme consumers (R version, the apt
package list, renv on/off, R CMD INSTALL . on/off, Chrome, submodules, render
profile). Label-gated extras are preserved: add preview:pdf, preview:docx,
or preview:revealjs to a PR to render those formats too, and clear freezer
to bypass the Quarto freeze cache.
Two repos can share single-source-of-truth content and keep both copies current without hand-bumping. The pair handles the two directions:
bump-submodule— for the side that vendors the other repo as a git submodule. A scheduled run advances the submodule to its upstream HEAD and opens a PR when it moved. (Used byUCD-SERG/lab-manual, which carriesd-morrison/ai-configas.ai-config.)sync-shared-fragments— for the side that can't add a submodule because the other repo already submodules it (a mutual submodule would recurse). Instead it vendors a pinned copy of the named files into adest-dir, records the source repo and commit in a JSON manifest, and opens a PR when the copy changes. (Used byd-morrison/ai-configto vendor the lab manual's authored fragments.) Don't hand-edit the vendored copies — edit them upstream and let the workflow refresh them; a consumer-side drift check can assert the copy matches the pinned commit.
Both reuse the open-sync-pr composite, which commits staged changes to a
reused automation branch and opens or updates one PR (no-op when nothing
changed). Schedule and workflow_dispatch triggers live in the caller stubs.
Path-filter or scope each side to the other repo's shared content (not its own
pointer/manifest) so the two auto-PRs don't ping-pong.
Releases are tagged vX.Y.Z; the vX major tag moves to the latest compatible
release. Consumers reference @v1. See CHANGELOG.md for what
changes as the @v1 tag moves and for any breaking-change migration steps.
Every third-party action is pinned to a full commit SHA, with the human-readable version in a trailing comment, e.g.:
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1This is GitHub's recommended hardening posture:
a SHA is immutable, so a re-pointed tag or a compromised upstream can't silently
change what runs — which matters here because jobs like the preview deploy run
with contents: write + pull-requests: write. .github/dependabot.yml
bumps these pins as upstreams publish releases, so they stay current instead of
freezing. When adding a new third-party action, pin it the same way.
First-party d-morrison/gha/*@v1 self-references and the examples/
templates intentionally track the @v1 major tag (so consumers ride the moving
major), and so are not SHA-pinned.
REVDEPS.md tracks repos that call these workflows, so consumers
can be notified before a breaking change. If your repo uses gha, please add
it there.
Reusable workflows in this public repo are callable from public repos automatically. A private consumer must allow access to this repo under Settings → Actions → General → Access before it can call these workflows.
This started as the pilot set (the byte-identical / near-identical workflow families) plus the PR-preview/publish family. Additional families (spell check, lint-changed-files, pr-commands, R-CMD-check) may be added later.