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 asMorrison-Lab/gha/<name>@vN(the major tag that capability currently recommends — see Versioning below). - 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: Morrison-Lab/gha/.github/workflows/check-bibliography-dois.yml@v2Pin to the major tag that capability currently recommends (@v2 in the
example above) — see Versioning below for the full breakdown,
since it varies per capability rather than defaulting uniformly to @v1. 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-secrets.yml |
Scan the repository's git history for committed credentials (API tokens, private keys, high-entropy password assignments) with gitleaks | version, checksums-sha256, config, paths-ignore, allowlist-file, log-opts, fail |
check-links.yml |
lychee link check with bundled config, PR skip-label, and auto-issue on main |
lychee-config, lychee-args, fail, fail-if-empty, create-issue-on-main, skip-label |
lint-yaml.yml |
yamllint over tracked YAML with a bundled config, plus a check that flags long run: script blocks as decomposition candidates |
python-version, config-file, paths-ignore, fail, max-script-lines, fail-on-long-scripts |
lint-markdown.yml |
markdownlint-cli2 over tracked Markdown with a bundled config, plus a check that flags long fenced code blocks as decomposition candidates | config-file, globs, paths-ignore, fail, max-code-block-lines, fail-on-long-code-blocks |
check-new-line-breaks.yml |
Advisory, diff-scoped check that flags newly-added Markdown lines packing more than one sentence/clause onto one source line | python-version, globs, paths-ignore, fail, clause-breaks, clause-min-length |
lint-qmd.yml |
markdownlint over the prose sections of tracked .qmd Quarto files (code chunks stripped, YAML front matter skipped natively) with a bundled default config; default 80-char line-length ceiling encourages semantic line breaks |
config-file, globs, paths-ignore, fail, max-line-length |
lint-changed-lines.yml |
lintr over only the lines a PR adds or modifies (not whole changed files), so lint rules can be adopted or tightened incrementally | path, install-quarto, extra-packages, install-package, fail |
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, no-changelog-label |
test-coverage.yml |
Measure R-package test coverage with covr and upload the Cobertura report to Codecov |
path, install-quarto, extra-packages, fail-ci-if-error, upload-test-results |
update-snapshots.yml |
Regenerate testthat snapshots, accept the new output, commit, and push — the workflow only verifies the suite passes against the accepted snapshots; their correctness is judged at PR review of the pushed commit | ref, pr-mode, julia, extra-packages, apt-packages, commit-message |
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, use-ai-config, plugin-marketplaces, plugins, reviewer, report-cost |
claude-code-review.yml |
Read-only Claude PR review (default stub runs on workflow_dispatch from @claude review; add pull_request in the caller for automatic reviews) |
pr-number, prompt-addendum, checkout-submodules, allowed-bots, track-progress, apt-packages, pip-packages, lab-manual, check-latex-macros, use-ai-config, plugin-marketplaces, plugins, report-cost, model |
gemini.yml |
Gemini CLI question-answering bot: replies to @gemini and @gemini-cli mentions on issues and PRs; routes @gemini review to gemini-code-review.yml. Read-only --- it does not edit files, push, or open PRs (see #367) |
setup-r, install-quarto, apt-packages, pip-packages, checkout-submodules, prompt-addendum, gemini-model, review-workflow-file |
gemini-code-review.yml |
Read-only Gemini PR code review (default stub runs on workflow_dispatch from @gemini review; add pull_request in the caller for automatic reviews) |
pr-number, prompt-addendum, checkout-submodules, gemini-model |
ai-code-review.yml |
Multi-agent PR review: picks one configured AI agent at random and dispatches its review workflow, falling through to the next candidate when one can't be dispatched | agents, pr-number, claude-review-workflow-file, gemini-review-workflow-file |
request-dependabot-review.yml |
Request review from configured reviewers when a PR's author matches a bot actor (Dependabot by default) | reviewers, bot-actor |
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 |
report-failure.yml |
File an issue when a watched job fails, or comment on the issue already open for that failure | title, body, labels |
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) | path, 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) |
— |
check-equation-renders.yml |
On the same workflow_run completion, crawl the build artifact with a headless browser and fail on equations MathJax can't render |
fail |
cleanup-pr-previews.yml |
Housekeeping: delete gh-pages preview directories for PRs that are no longer open, and (optionally) orphan-squash gh-pages to one commit so deleted snapshots stop bloating the repo |
preview-dir, compact-history |
altdoc-multiversion-docs.yml |
Render an altdoc-based R package's Quarto docs and deploy multiple versions side by side on gh-pages (/dev/, /latest-tag/, /vX.Y.Z/, plus PR previews and a root redirect) |
r-packages, needs, apt-packages, setup-julia, checkout-submodules, default-branch, quarto-config-path, docs-base-url, preview-branch, timeout-minutes, rewrite-pr-preview-links, rewrite-issue-links, dispatch-version, dispatch-release-tag, legacy-paths, version-dropdown-title-template, version-in-navbar-title |
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 |
sync-upstream.yml |
Merge an upstream repo's branch into a fork and open a PR when the merge brings changes — keeps a fork current while preserving its own changes | upstream-repo, upstream-branch, base-branch, pr-branch, fail-on-conflict |
bump-dev-version.yml |
Bump an R package's DESCRIPTION dev-version counter after every merge to main, and open/auto-merge a PR to carry it in -- so PRs never need to touch Version: themselves |
description-path, base-branch, pr-branch, auto-merge, dry-run |
version-check.yml |
Fail a PR whose DESCRIPTION Version: differs from the base branch's -- pairs with bump-dev-version.yml |
description-path, no-version-increment-label, bump-branch |
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.report-failure(files or updates the issue tracking a failing workflow) → grantissues: writeon the reporting job only; the job it watches keeps its own permissions.summary(comments on issues, calls the models API) → grantissues: write,models: read,contents: read.check-bibliography-dois,check-non-standard-chars,check-phi,check-secrets,check-new-line-breaks,test-coverage→ onlycontents: read(the default), so nopermissions:block is needed.test-coverageadditionally takes an optionalCODECOV_TOKENsecret, passed through the caller'ssecrets:block.update-snapshots(pushes the snapshot-update commit back to the branch) → grantcontents: write.quarto-publish(deploys to thegh-pagesbranch, which Pages serves) → grantcontents: write, and set Settings → Pages → Source = "Deploy from a branch", branchgh-pages/(root)once. Grantcontents: writeeven withdeploy: false— the deploy job is part of the workflow, so the caller must grant its permissions even when it is skipped.altdoc-multiversion-docs(deploys togh-pages, comments PR previews, and rewrites rendered links) → grantcontents: write,pull-requests: write, andissues: write, and set Settings → Pages → Source = "Deploy from a branch", branchgh-pages/(root)once.claude(pushes branches, opens PRs, dispatches the review workflow) → grantcontents: write,pull-requests: write,issues: write,id-token: write,actions: write, and add either theCLAUDE_CODE_OAUTH_TOKENorANTHROPIC_API_KEYsecret.- 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. When the secret is absent and Claude does edit a workflow file, the rejected push is reported as an error naming this secret, and Claude's commits are posted to the thread as agit format-patchso they survive the run. - 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 either theCLAUDE_CODE_OAUTH_TOKENorANTHROPIC_API_KEYsecret.- 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.check-equation-renders(downloads the build artifact, read-only) → grantcontents: read,actions: read.cleanup-pr-previews(commits deletions togh-pages) → grantcontents: write,pull-requests: read.bump-submodule,sync-shared-fragments,sync-upstream(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;sync-upstreamtakes anUPSTREAM_TOKENfor a private upstream. Add aWORKFLOW_TOKENonly to push to a protected branch; otherwise pushes fall back toGITHUB_TOKEN.request-dependabot-review(requests a reviewer on the PR) → grantpull-requests: write.bump-dev-version(opens/auto-merges a PR) → grantcontents: write,pull-requests: write, and the same "Allow GitHub Actions to create and approve pull requests" setting as above; enable "Allow auto-merge" too for its defaultauto-merge: true. Add aWORKFLOW_TOKENonly to push to a protected branch.version-check(read-only) → onlypull-requests: read,contents: read.
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.
The examples/claude-code-review.yml stub defaults to this mention-triggered
path only (no automatic pull_request trigger). Add pull_request in that
stub if you want automatic review on each PR update.
You can also start a review directly, without waking the @claude agent, by
commenting /review at the start of a PR comment — but that path is opt-in:
enable the issue_comment trigger in examples/claude-code-review.yml first.
Then claude-code-review.yml listens for that comment and re-dispatches its own
workflow_dispatch review of the PR, and it works for
OWNER/MEMBER/COLLABORATOR commenters once the workflow is on your default
branch. It's a slash command rather than an @claude review mention on
purpose: any @claude substring would also trigger claude.yml, so the slash
command keeps the direct path independent.
GHA sessions (both claude.yml and claude-code-review.yml) run as headless
CI jobs and cannot be remote-controlled or observed from the
claude.ai web interface. The CLI's "Join session" feature
requires a live interactive terminal; anthropics/claude-code-action has no
parameter to enable it, and GitHub Actions runners don't expose that hook.
The table below lists what is available instead. Its "Action argument" column
gives the argument passed to anthropics/claude-code-action; these are not
caller-facing workflow_call inputs unless the "Caller-configurable?" column
says so.
| Feature | Action argument | Caller-configurable? |
|---|---|---|
| Live progress tracking comment on the PR | track_progress |
Yes — driven by the track-progress input of claude-code-review.yml (default false; tag mode with tracking comment and inline-comment tool when true, agent/summary-only mode when false). Not used in claude.yml. See track-progress warning in the inputs table: tag mode exposes git write tools until anthropics/claude-code-action#1415 lands. |
| Full Claude SDK output in the job log | show_full_output |
Yes — driven by the show-full-output input of claude-code-review.yml (note the hyphen; off by default, turn on to diagnose silent auth / quota failures). Not surfaced in claude.yml. |
| Resume a prior session | session_id (internal step output of anthropics/claude-code-action) + --resume in claude_args |
No — neither reusable workflow declares session_id as a workflow_call output, so session resume is not available to consumers of claude.yml or claude-code-review.yml. |
| Dollar cost of the run | n/a (action output, not an argument) — total_cost_usd on the execution output's result event |
Yes, indirectly — verified against anthropics/claude-code-action v1.0.162 (the SHA this repo pins): src/entrypoints/format-turns.ts writes the cost only to GITHUB_STEP_SUMMARY, and src/github/operations/comment-logic.ts's comment builder receives total_cost_usd but never reads it when composing the comment. Both workflows extract that output and post it in a comment instead, gated on the report-cost input (default true). |
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.
check-secrets is check-phi's counterpart for credentials.
check-phi detects identifiers and has no notion of a password or a token,
so a committed credential passes it cleanly.
This one runs gitleaks over the
repository,
catching API tokens, private keys, and high-entropy password assignments.
It shares check-phi's stance in most respects,
and departs from it in three that matter.
- It scans history, not the diff.
Every other check here is diff-scoped,
so a fixture committed long ago does not re-trip it.
That is the wrong default for a credential:
a secret committed and then removed in a later commit is still exposed,
because the orphaned commit stays fetchable through the GitHub API until
the repository is garbage-collected.
So the caller must check out with
fetch-depth: 0, and a shallow clone is refused rather than reported clean on a partial scan. The scan reaches further than "history" suggests, too: gitleaks defaults togit log -p -U0 --full-history --all, so it covers every ref the checkout holds rather than onlyHEAD's ancestry. A finding can therefore name a commit that is not an ancestor of the PR's own head, which is right for a credential -- an exposed one is exposed wherever it sits. Narrow it withlog-optsonly deliberately. - It blocks by default (
fail: true), where the advisory prose checks warn. A leaked credential is not a style nit. - Its
paths-ignorepatterns are Go regexes, not globs, because they become gitleaks allowlist entries directly, and gitleaks matches them unanchored. Sodocssuppresses every path containing that substring,mydocs-secrets.envincluded; anchor with^when that matters. Writetests/fixtures/, nottests/fixtures/**. The two glob forms fail differently, which is why the check warns rather than trusting you to notice:**does not compile, since Go rejects nested repetition, but a trailing/*is a perfectly valid regex that silently widens the match.
Otherwise it behaves as check-phi does.
Values are never printed -- a credential in a CI log is still a
credential -- so findings report only the rule, file:line, and the commit.
Suppress a false positive with a gitleaks:allow comment on the line,
a fingerprint in a .gitleaksignore file,
a regex in the allowlist-file
(defaults to .github/secrets-allowlist.txt when present;
one regex per line, and a comma there is regex syntax rather than a
separator, so a {16,20} quantifier is safe),
or a path in paths-ignore.
Site-specific credential formats the default ruleset does not know go in a
gitleaks TOML named by the config input (.gitleaks.toml by default),
which the generated config extends rather than replaces.
Two limits worth stating plainly.
It complements GitHub's native secret scanning; it does not replace it. That is a repository setting, it evaluates pushes rather than running as a PR check, and no reusable workflow can supply it. Enable both.
Neither substitutes for rotating an exposed credential. Rewriting history does not un-expose one. Treat any value this check names as compromised, and rotate it before doing anything else.
The vendor's own action is proprietary:
its action.yml carries a commercial EULA header,
and its README states GITLEAKS_LICENSE is "required for organizations,
not required for user accounts".
Every consumer of this repo is an organization.
The gitleaks CLI is MIT,
so the composite installs the official release binary instead.
Integrity comes from one pinned constant:
the release's own checksums.txt is fetched and compared against the
checksums-sha256 input,
and only then trusted to verify the platform tarball --
one value covering every architecture.
Bump version and checksums-sha256 together.
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 four cooperating
workflows — install all four 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.check-equation-renders.yml— also triggered onworkflow_runcompletion of the build. Downloads the same artifact and crawls it with a headless browser, failing when MathJax can't typeset an equation — a failure mode invisible to the Quarto/pandoc build log, since MathJax only runs client-side. Runs independently of the deploy (nogh-pageswrite needed), not sequenced after it.cleanup-pr-previews.yml(housekeeping) — scheduled. Removes preview directories for PRs that have closed. Setcompact-history: trueto also orphan-squashgh-pagesto a single commit each run, so the deleted snapshots don't accumulate and bloat the repo (branch-based Pages only).
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 and the equation-check stub's
on: workflow_run: workflows:value must match the build stub'sname:(all 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.
Three workflows keep a repo current with content that lives elsewhere, without hand-bumping. The first two are the two directions of sharing single-source-of- truth content between a pair of repos; the third tracks an upstream a fork was cut from.
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 carriesMorrison-Lab/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 byMorrison-Lab/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.sync-upstream— for a fork that tracks the project it was cut from. A scheduled run merges the upstream branch into a fork-owned automation branch and opens a PR when the merge brings changes, so the fork's own changes are preserved and upstream's updates are reviewed before they land. On a clean merge the PR is mergeable; on a conflict it carries the conflict markers for manual resolution (or setfail-on-conflictto fail the run instead). (Used byd-morrison/altdoc, a fork ofetiennebacher/altdoc.)
All three 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.
For bump-submodule/sync-shared-fragments, 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. @v1 was frozen at the pre-2.0.0 snapshot when the breaking
quarto-publish change cut @v2, so any capability pinned there has picked up
no fixes since — including non-breaking ones, like cleanup-pr-previews's
compact-history input, which does not exist at @v1 at all. Pin
preview.yml, preview-deploy.yml, cleanup-pr-previews.yml, and
quarto-publish.yml to @v2; test-coverage.yml, check-equation-renders.yml,
lint-yaml.yml, lint-markdown.yml, lint-qmd.yml, lint-changed-lines.yml,
check-new-line-breaks.yml, and check-secrets.yml only ever
shipped at @v2 (too new to exist at the frozen @v1 tag). quarto-publish.yml additionally has a genuine
behavioral fork: @v1 deploys via the GitHub Actions Pages artifact, while
@v2 deploys to the gh-pages branch instead — required alongside the
PR-preview family (preview.yml / preview-deploy.yml), since Pages can only
have one Source. check-bibliography-dois.yml, check-phi.yml,
check-links.yml, check-non-standard-chars.yml, claude.yml,
claude-code-review.yml, and update-snapshots.yml also pin @v2: each
picked up a real fix since the freeze (a dependency-pin bump, a new input, or
a security fix) that a consumer still on @v1 would miss (audited in
gha#182).
request-dependabot-review.yml only ever shipped at @v2 too (it postdates
the freeze — see gha#252), as
does sync-upstream.yml (added after the freeze — see
gha#254),
altdoc-multiversion-docs.yml (added after the freeze), and
report-failure.yml (added after the freeze — see
gha#325).
bump-dev-version.yml and version-check.yml postdate the freeze too (added
in gha#388); pin both to
@v2.
summary.yml, bump-submodule.yml, and sync-shared-fragments.yml were
audited in the same pass and found unchanged since the freeze, so @v1
remains current for them. check-news.yml was initially grouped with them,
but later gained the configurable no-changelog-label input at
gha#143 -- pin it to @v2
too. See CHANGELOG.md for
what changes as a major tag moves and for any breaking-change migration steps.
A major tag no longer slides automatically on every push to main — merging a
change does not, by itself, change what any consumer pinned to @v1/@v2
picks up next. Advancing the tag is a deliberate, manual step, so a change can
be tried out before every consumer has to deal with it:
- Merge the change to
main. - Optionally, validate it against one or a few consumer repos first: point a
consumer's
uses:line at@main, at a specific commit SHA, or temporarily at a feature branch of this repo, then let that consumer's own CI run against the unreleased change. - Once you're confident, advance the shared major tag to
main's current tip — either by runningslide-major-tag.ymlviaworkflow_dispatch(Actions tab → "Slide major-version tag" → Run workflow, frommain), or withgit tag -f/git push --forcedirectly (theai-configrepo'sslide-tagskill automates this). Every consumer pinned to that tag picks up the change the next time its CI runs.
A brand-new capability's own PR merging to main does not make it usable
at @v2 yet. If a consumer repo's PR needs to reference
Morrison-Lab/gha/.github/workflows/<new-workflow>.yml@v2 right after that
workflow's own PR merged here, check whether @v2 has actually been
advanced past that merge first (git log -1 refs/tags/v2 vs. main) --- a
consumer referencing @v2 before the slide gets a workflow-not-found error,
not a stale-but-working reference. (gha#300/ai-config#703, 2026-07-25:
check-new-line-breaks merged here, but @v2 was still 11 commits behind;
the tag had to be slid via step 3 above before ai-config's own migration PR
could actually resolve it.)
Changelog entries are added as fragment files under
changelog.d/ (one per PR, so parallel PRs never conflict on the
shared changelog) and collated into CHANGELOG.md at release time — see
changelog.d/README.md.
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0This 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 Morrison-Lab/gha/* self-references and most examples/
templates intentionally track the moving major tag (currently @v1, except
preview.yml, preview-deploy.yml, cleanup-pr-previews.yml,
quarto-publish.yml, test-coverage.yml, check-equation-renders.yml,
check-bibliography-dois.yml, check-phi.yml, check-links.yml,
check-non-standard-chars.yml, claude.yml, claude-code-review.yml,
update-snapshots.yml, lint-yaml.yml, lint-markdown.yml,
lint-qmd.yml, lint-changed-lines.yml, check-new-line-breaks.yml,
check-secrets.yml, request-dependabot-review.yml,
sync-upstream.yml, check-news.yml, altdoc-multiversion-docs.yml,
report-failure.yml, gemini.yml, gemini-code-review.yml,
ai-code-review.yml, bump-dev-version.yml, and
version-check.yml at @v2 -- see the
Versioning section above), and so are not SHA-pinned.
Every job that runs steps sets timeout-minutes, so a hung step fails in
minutes instead of occupying a runner until GitHub's six-hour default expires.
The values are deliberately generous -- roughly 10 minutes for gate and
dispatch jobs, 20 for checks and lints, 45 for builds and deploys, 60 for the
agent workflows -- because the goal is catching a hang, not budgeting a normal
run.
A job that calls a reusable workflow cannot set timeout-minutes itself
(GitHub rejects the key on a uses: job), so such a job inherits whatever
timeout the called workflow's own job declares.
altdoc-multiversion-docs.yml additionally exposes its timeout as a
workflow_call input, which is the pattern to follow if a consumer ever needs
to raise one.
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.