fix(claude-code-review): always use agent mode to prevent unauthorized commits - #134
Conversation
…d commits Tag mode (track_progress: true) hardcodes git write tools (git add, git commit, git rm, git-push.sh) into ALLOWED_TOOLS via the action's TypeScript, regardless of --disallowedTools in claude_args. The CLI flag cannot override an env var set by the action itself, so the review bot was able to — and did — push commits to PR branches during review runs. Confirmed: serodynamics PR #175, commit 02af72b, pushed by workflow run 28271356338 during what was supposed to be a read-only review. Fix: set track_progress: false for all events. In agent mode, ALLOWED_TOOLS is built solely from claude_args, so no git write tools are ever available. Trade-off: agent mode does not auto-post a tracking comment or initialize the inline-comment tool (claude-code-action #635). Compensate by extending the "Post review comment" step (previously dispatch-only) to run on all events, so pull_request reviews still appear on the PR as top-level comments with summary-format findings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6sydNrkDLeYxrpvmWRJkT
This comment has been minimized.
This comment has been minimized.
Update two comments that still referenced tag-mode behavior after the switch to always-agent-mode: - claude_args block: remove 'tag mode's tracking-comment tool is preserved' and 'only initialized in tag mode'; describe agent-mode reality instead. - Collapse step: update step name reference from 'Post review comment for dispatched run' to 'Post review comment' (renamed in prior commit) and note it now covers both dispatched and pull_request runs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6sydNrkDLeYxrpvmWRJkT
This comment has been minimized.
This comment has been minimized.
|
@claude make mode an input option that consumers of this action can set |
|
👀 Picked up by workflow run #28571359079. Setup runs first; Claude itself responds after that. |
|
Working on this — paws off until I'm done. Generated by Claude Code |
This comment has been minimized.
This comment has been minimized.
…pt into tag mode Adds a boolean `track-progress` input (default: false) so callers can opt into tag mode when they want a live tracking comment and inline review comments. The default stays false (agent mode) — safe, no git write tools. Tag mode should only be enabled once anthropics/claude-code-action#1415 ships a read_only input; until then, tag mode exposes git write tools and the reviewer may push commits (serodynamics PR #175, commit 02af72b). Also gates the "Post review comment" manual-post step on agent mode so it doesn't duplicate the auto-posted tracking comment when tag mode is active. Fixes #140 (partially — the tracking issue stays open until the upstream read_only input lands and we can enable tag mode safely by default). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6sydNrkDLeYxrpvmWRJkT
This comment has been minimized.
This comment has been minimized.
…nce page Per CLAUDE.md: new workflow_call inputs need doc sync at three sites. - README.md: add track-progress to key-inputs column; update the track_progress feature table row to note it is now caller-configurable. - website/workflows.qmd: add track-progress to key-inputs column. - website/reference/claude-code-review.qmd: add row to Inputs table with safety warning; add commented usage line in the Example block. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6sydNrkDLeYxrpvmWRJkT
This comment has been minimized.
This comment has been minimized.
Em dash (U+2014) flagged by check-chars. Replace with ASCII hyphen. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6sydNrkDLeYxrpvmWRJkT
This comment has been minimized.
This comment has been minimized.
|
Claude finished review — View run I'll stop here and wait for the agents to complete or the scheduled wakeup at 07:13. |
|
@claude review Generated by Claude Code |
|
👀 Picked up by workflow run #28572166679. Setup runs first; Claude itself responds after that. |
Code Review — PR #134 (follow-up, since commit
|
|
Claude finished review — View run The review has been posted (replacing my accidental test comment) at #134 (comment). Summary: I reviewed the changes since the last approved round (commit
Both are minor/non-blocking documentation-accuracy issues, not functional bugs — the security fix and the new feature's logic are both correct. Verdict: Needs minor revisions. |
- Update the "Explain and fold tracking comment when canceled" step comment: replace "we now always run in agent mode (no tracking comment)" with the accurate phrasing — agent mode is the default, but callers can opt into tag mode via `track-progress: true`. - Move `### Fixed` block to after `### Changed` in CHANGELOG.md to match the established Added→Changed→Fixed ordering convention. - Change `inputs['track-progress']` to `inputs.track-progress` in the "Post review comment" step's `if:` condition to match the dot notation used everywhere else in the workflow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6sydNrkDLeYxrpvmWRJkT
|
Claude finished @d-morrison's task in 56s —— View job Code Review — PR #134 (follow-up, since commit
|
2026-07-02 07:42 UTC |
All notable changes to `d-morrison/gha` are documented here.
This repo uses a **moving major tag** (`v1`) for consumers, following the
[`r-lib/actions`](https://github.com/r-lib/actions) convention: the `v1` tag
moves forward as non-breaking fixes land, and consumers pin to `@v1`. Each
release is also tagged with a full `vX.Y.Z` so a specific point can be pinned
if needed. Breaking changes bump the major tag (`v2`, …) and are called out
below with migration steps.
## [Unreleased]
### Changed
- **`claude-code-review.yml` now bakes in SERG lab manual enforcement** (#170).
The reusable review prompt now always evaluates a PR against the
[UCD-SERG lab manual](https://ucd-serg.github.io/lab-manual/) — coding
style, coding practices (including function decomposition/length limits),
and repository conventions — so every consumer repo gets this check
automatically, without needing to pass a `prompt-addendum`. Mirrors the
wording already in this repo's own `CLAUDE.md` "Code review guidelines".
### Added
- **`test-coverage` — R-package test coverage with Codecov upload** (#147). A new
composite (`test-coverage/action.yml`) and reusable workflow
(`.github/workflows/test-coverage.yml`) that set up R and dependencies, run
`covr::package_coverage()`, and upload the Cobertura report with
`codecov/codecov-action`. Adapts the canonical `r-lib/actions`
`test-coverage.yaml` example into the repo's composite-plus-wrapper shape.
Inputs: `path` (package root, defaults to repo root), `install-quarto`,
`extra-packages`, and `fail-ci-if-error` (`'auto'` by default, applying the
r-lib heuristic -- fail on non-PR events, and on PRs only when a token is set,
since tokenless PR uploads are flaky -- or force `'true'`/`'false'`); the
optional `CODECOV_TOKEN` secret is passed through the caller's `secrets:`
block. See `examples/test-coverage.yml` for the caller stub.
- **`check-equation-renders` — catch equations MathJax can't render in PR previews**
(#159). A new fourth leg of the PR-preview family: a composite
(`check-equation-renders/action.yml`) that crawls a built Quarto/HTML site with a
headless Chromium (Playwright), lets MathJax finish typesetting each page, and
fails on either of the two ways MathJax signals a broken formula: a hard parse
error (a `[data-mjx-error]` node), or an undefined macro, which MathJax renders
as literal, unresolved `\command` text rather than a hard error (no error node
to find, so the check looks for the raw command name surviving into the
rendered output instead). Wired into
the family as a reusable workflow (`.github/workflows/check-equation-renders.yml`)
triggered the same way as `preview-deploy.yml` (`workflow_run` on the build
workflow's completion), downloading the `pr-preview-site` artifact directly rather
than depending on the deploy. Motivated by a broken equation
(`d-morrison/rme#954`) that shipped silently: Quarto's `html-math-method: mathjax`
embeds raw TeX unchanged in the static HTML, and MathJax only parses it
client-side, so a bad equation produces no warning in the Quarto/pandoc build log.
Ships at `@v2` (too new for the frozen `@v1` tag), like `test-coverage`. See
`examples/check-equation-renders.yml` for the caller stub.
- **`claude-code-review` accepts `apt-packages`/`pip-packages` inputs** (#161),
mirroring the inputs `claude.yml` already had. Lets a math-heavy consumer
repo install a computer algebra system (e.g. `apt-packages: maxima`,
`pip-packages: sympy`) so the reviewer's `Bash` tool can symbolically check
a derivation instead of eyeballing the algebra. Both default to `''`
(no-op), so this is backward compatible for every existing caller. The
install steps for both workflows are now factored into a shared
`.github/actions/install-packages` composite rather than duplicated.
- **Added `.gitattributes`** (#163). Normalizes line endings (`text=auto
eol=lf`, explicit for `*.sh`) and union-merges `CHANGELOG.md` so two
sibling PRs appending entries under `## [Unreleased]` merge cleanly
instead of conflicting on adjacent insertions (mirrors `d-morrison/ai-config`).
- **`claude-code-review` gains a `track-progress` input** (#134). Consumers
can now opt into tag mode (live tracking comment + inline-comment tool) by
setting `track-progress: true`. The default remains `false` (agent mode),
which is safe — agent mode never grants git write tools, preventing the
review bot from pushing commits to PR branches. Only set `track-progress:
true` once [anthropics/claude-code-action#1415](https://github.com/anthropics/claude-code-action/issues/1415)
ships a `read_only` input; until then, tag mode exposes git write tools.
- **`/review` comment trigger for `claude-code-review`.** Commenting `/review`
at the start of a PR comment now starts a review of that PR on demand, without
routing through `claude.yml`'s `@claude` agent. The review caller listens for
the comment (gated to `OWNER`/`MEMBER`/`COLLABORATOR` authors) and re-dispatches
its own `workflow_dispatch` review, reusing the existing dispatched-review flow
— so the reusable `claude-code-review.yml` is unchanged and consumers get the
tag by updating their `examples/claude-code-review.yml` stub. It's a slash
command rather than an `@claude review` mention on purpose: any `@claude`
substring also wakes `claude.yml`, so the slash command keeps the direct path
independent. Works once the workflow is on the default branch (`gh workflow
run` requires it).
### Changed
- **`claude-code-review` grants the reviewer `Bash(python3 <file>)`.** The
review agent could previously only trace a Python script's logic by eye,
since its `--allowedTools` covered just the inline-comment tool plus the
action's base allowlist (Read/Glob/Grep and narrow git-read Bash) — no way
to actually execute the script under review (rme#970). Scoped to running an
existing file under the checkout (`-c`/`-m` denied, so inline/module code
execution stays blocked) — same-repo PRs give this job
`CLAUDE_CODE_OAUTH_TOKEN`/`ANTHROPIC_API_KEY` as secrets, so unrestricted
`python3:*` would have been a real capability widening beyond the existing
git-read-only sandbox, not just a git-push question. It can now verify a
script's behavior instead of guessing from source alone (#154).
- **`claude-code-review` now honors an explicit review request on a draft PR.**
A dispatched review (an `@claude review` comment routed here by `claude.yml`,
`claude.yml`'s post-push re-dispatch, the issue-trigger draft PR, or a manual
dispatch) already bypassed the workflow's draft-skip `if:` gate, but the
code-review skill's *own* don't-review-drafts stop condition still made the
agent refuse ("currently a draft … I will not proceed"), so an explicit
`@claude review` on a draft produced a refusal instead of a review. The
dispatched-run prompt now overrides that stop condition so an
explicitly-requested review runs even on a draft. Automatic `pull_request`
reviews still skip drafts (their `if:` gate never reaches the agent on a
draft), so this only widens the dispatched path.
- **`claude-code-review` no longer flags cosmetic source-only formatting that
renders identically** (#261). The review prompt now tells the reviewer to skip
raw-source line-wrap position and line-length nits on Markdown/text prose when
the rendered output is unchanged, and to flag only genuine prose problems
(ambiguity, meaning-changing grammar, broken links or markup, factual errors).
This cuts review-round churn from cosmetic wrapping nits.
### Fixed
- **`claude-code-review`'s stub guard no longer false-fails complete reviews**
(#173). The `### Verdict`-heading check added for the silent-stub problem was
too strict: the `code-review` plugin states its conclusion as a `Verdict:`
label (not the `### Verdict` heading the prompt requests), and in agent mode
it can land in an earlier assistant message than the last — so every
push-triggered review red-X'd (gha#175) even when it posted a full review with
a verdict. `fail-check` now scans **all** of the run's assistant text (not just
the final block) and accepts a verdict line in any heading/label/bold form
(`### Verdict`, `**Verdict:**`, `Verdict:`), and the comment posted back to the
PR uses the verdict-bearing block rather than the final one, so a trailing
wrap-up message ("I've posted my findings") isn't mistaken for the review body
(sparta#594). A genuine stub still states no verdict and still fails,
preserving the original intent of catching the silent-stub failure mode
([`Lacaedemon/sparta#590`](https://github.com/Lacaedemon/sparta/issues/590)).
- **`claude-code-review`'s pass/fail check now catches stub reviews, not just
API errors** (#171). The reusable workflow's `fail-check` step previously
only inspected `is_error`/`subtype` on the SDK result, so a run that
reported success while actually exiting on an orchestration placeholder
(e.g. "Waiting for both background agents... before proceeding to the next
review steps") still posted that placeholder as the finished review and the
check stayed green — the same silent-stub failure mode reported upstream in
[`Lacaedemon/sparta#590`](https://github.com/Lacaedemon/sparta/issues/590).
`fail-check` now also extracts the run's final review text and fails the
check if it's empty (including whitespace-only) or missing the `### Verdict`
heading this workflow's own prompt requires of every finished review —
a stub is narration, never a finished review, so it can't contain that
heading, which catches every observed stub phrasing without having to
enumerate them individually.
- **`claude-code-review` no longer pushes unauthorized commits to PR
branches** (#134). Tag mode (`track_progress: true`) in `claude-code-action`
hardcodes git write tools into `ALLOWED_TOOLS` regardless of `--disallowedTools`,
causing the review bot to commit and push during review runs (confirmed:
serodynamics PR #175, commit `02af72b`). The workflow now defaults to agent
mode, which builds `ALLOWED_TOOLS` solely from `claude_args` with no git write
tools. Trade-off: reviews are summary-only (no inline comments) in the default
mode; opt into tag mode with the new `track-progress` input when the upstream
fix is available.
## [2.0.0] - 2026-06-25
### Breaking
- **`quarto-publish` now deploys to the `gh-pages` branch** instead of the
GitHub Pages artifact (#118, #120). Consumers must set Settings → Pages →
Source = "Deploy from a branch", branch `gh-pages` / `(root)`, and grant the
caller `contents: write` (dropping `pages: write` + `id-token: write`) — still
required with `deploy: false`. This makes `quarto-publish` compatible with the
branch-based PR-preview family, so a repo can ship both a main site and PR
previews. Full migration steps are in the `quarto-publish` entry under Added.
`@v1` stays frozen on the Pages-artifact deploy for consumers not ready to
migrate.
### Added
- Dark mode for the documentation website (#114) — `website/_quarto.yml` now
pairs the `cosmo` light theme with the `darkly` dark theme, so the site renders
a light/dark toggle in the navbar and respects the reader's system preference.
- Documentation website (#100) — a Quarto site under `website/` that documents
every reusable workflow (overview, a per-action reference page with full input
tables, permissions, and versioning). It is built and shipped by the repo's own
actions: `quarto-publish` deploys it to GitHub Pages on `main`, and the
`preview` family renders a per-PR preview.
- `preview` gains a `path` input (#100) — the project directory to render
(the dir holding `_quarto.yml`), defaulting to the repo root. This brings
`preview` to parity with `quarto-publish` and lets a site that lives in a
subdirectory (like this repo's `website/`) get a PR preview. Backward
compatible: existing callers that render the repo root need no change.
- Shared-content sync family (#57) — keeps guidance shared between repos current
in both directions, via two reusable workflows and a shared helper:
- `bump-submodule.yml` — update a named submodule to its upstream HEAD and open
a PR when the pointer moves (one direction; e.g. the lab manual tracking
`.ai-config`).
- `sync-shared-fragments.yml` — vendor a set of files from an upstream repo,
pinned to a commit and recorded in a JSON manifest, and open a PR when they
change (the other direction; avoids a recursive mutual submodule).
- `open-sync-pr` composite — the commit-and-open-PR helper both workflows
reuse: commits staged changes to a reused automation branch and opens or
updates the PR, no-op when nothing changed. First consumers:
`UCD-SERG/lab-manual` and `d-morrison/ai-config`.
- `quarto-publish` — render a Quarto site and deploy it to the `gh-pages`
branch, which GitHub Pages serves. A composite (`quarto-publish/action.yml`)
sets up Quarto (optionally R/renv and TinyTeX) and renders a project at a
given `path` into `<path>/<output-dir>` (default `_site`). The reusable
workflow (`quarto-publish.yml`) deploys that output to `gh-pages` with
`clean-exclude: pr-preview/`, so a main-site deploy never wipes the preview
family's per-PR sites; it also offers optional submodule init and a
`pre-render-artifact` input so a caller can inject build-time assets (e.g.
recorded media) before render. Callers grant `contents: write` (still
required with `deploy: false`, since the deploy job is part of the workflow)
and set Pages Source = "Deploy from a branch", branch `gh-pages`. First
consumer: `Lacaedemon/sparta` (#37).
**Breaking change for early `@v1` adopters (#117).** An earlier interim
version deployed via `actions/deploy-pages` and needed Pages Source =
"GitHub Actions". That is incompatible with the PR-preview family, which is
branch-based, so previews 404'd. Switching `quarto-publish` to a `gh-pages`
deploy makes publish and preview consistent. To migrate: (1) set Settings ->
Pages -> Source = "Deploy from a branch", branch `gh-pages` / `(root)`; and
(2) change the caller's job permissions from `pages: write` + `id-token:
write` to `contents: write` (see `examples/quarto-publish.yml`).
- PR-preview / publish family (#33) — centralizes the three-workflow preview
pipeline rme carried inline:
- `preview` composite action + `preview.yml` reusable workflow — build half;
renders the Quarto site read-only in the (possibly fork) PR context and
uploads it + PR metadata as an artifact. Parameterized for non-rme
consumers (R version, apt packages, renv on/off, local-package install,
Chrome, submodules, render profile). Writes PR metadata **after** checkout
so `git clean -ffdx` can't wipe it from the artifact (d-morrison/rme#913),
and keeps the `preview:pdf`/`preview:docx`/`preview:revealjs` and
`clear freezer` label gates.
- `preview-deploy.yml` reusable workflow — deploy half; on `workflow_run`
completion publishes the artifact to `gh-pages` in the base-repo context
and comments the preview link. Kept split from the build half so untrusted
fork code never holds write permissions (the trust boundary).
- `cleanup-pr-previews.yml` reusable workflow — scheduled housekeeping that
deletes preview directories for closed PRs.
- `check-phi` — scans pull requests (added lines only; whole tree on `push`)
for content that looks like PHI: US Social Security numbers, medical record
numbers, dates of birth, and PHI-suggestive column headers in delimited data
files. Matched values are never printed to the log; false positives are
suppressed via a `phi-allow` line comment or a regex allowlist file. The
`phone`/`email` detectors are available but off by default.
- `CHANGELOG.md` (this file) — records what changes as the `@v1` tag moves, so
consumers can see what they picked up.
- `REVDEPS.md` — tracks repos that consume these workflows so breaking changes
can be announced. See the file for how to register.
- `.github/actions/checkout-submodules` composite action — centralizes the
submodule-init logic (the `SUBMODULES_TOKEN` `insteadOf` rewrite and the
anonymous-clone fallback) shared by the `claude` and `claude-code-review`
reusable workflows (#25).
### Changed
- Multi-line `run:` blocks in the composite actions and reusable workflows now
declare `set -euo pipefail` explicitly. GitHub already runs `shell: bash`
with `-eo pipefail`; the net new protection is `nounset` (unset-variable
typos now fail fast), plus consistency with the rest of the script logic.
- `claude` and `claude-code-review` no longer carry duplicate `Checkout
submodules` steps; both call the shared `checkout-submodules` action instead,
so the token-rewrite logic lives in one place (#25).
- `check-bibliography-dois` now collects `.bib` files NUL-delimited into a bash
array, so bibliography paths containing spaces are passed to the checker as
intact single arguments instead of word-splitting (#30).
- `claude-code-review`'s prompt now instructs the reviewer to watch for AI
hallucinations — fabricated functions/arguments/APIs, invented references,
DOIs, or URLs, plausible-but-unreal file paths and constants, and comments
that describe behavior the code doesn't implement — and to verify questionable
symbols against the codebase rather than assuming they exist (#56).
- `claude` now reproduces qwt's late-comment dedup so a follow-up `@claude`
comment absorbed by a still-running session isn't double-handled by the
duplicate run it also queued: a "Skip if this comment was already handled"
pre-step bows out when the triggering comment already carries a github-actions
🚀 marker, the agent emits a `<!-- claude-absorbed: … -->` marker listing the
comments it absorbed by polling, and a post-step reacts 🚀 to each so their own
queued runs short-circuit. A companion step re-dispatches a review for a late
`@claude review` that a deduped run would otherwise have dropped. Additive —
no consumer input changes (#44, ported from qwt #73/#90/#95).
- `claude-code-review` gained an `allowed-bots` input (default
`github-actions[bot]`, previously hard-coded) so a consumer can widen the
accepted dispatch actors (e.g. `github-actions[bot],claude`), and a
"Skip self-review when the PR edits this workflow" step that detects (via
`github.workflow_ref`) a PR modifying the caller's review workflow and skips
the review — which would otherwise 401 on the action's workflow-validation
until merged — instead of posting a failed check (#45, ported from qwt).
### Fixed
- **Example caller stubs now pass secrets explicitly instead of `secrets:
inherit`.** GitHub only inherits org/repo secrets into a reusable workflow
owned by the *same* org/user, so a cross-owner consumer (e.g. a `UCD-SERG`-org
repo calling these `d-morrison`-user-owned workflows) inherited an empty
`CLAUDE_CODE_OAUTH_TOKEN` and every `@claude` run failed env-validation
("… is required when using direct Anthropic API"). `examples/claude.yml` and
`examples/claude-code-review.yml` now pass `CLAUDE_CODE_OAUTH_TOKEN` (and the
optional `SUBMODULES_TOKEN` / `WORKFLOW_TOKEN`) explicitly, which resolves
caller-side and works regardless of owner. Existing consumers copied from the
old stubs must make the same change (#49).
- `claude-code-review` now sets `allowed_bots: github-actions[bot]`, so the
review `claude.yml` re-dispatches after an `@claude` run pushes commits can
actually run. The action's agent mode (used by `workflow_dispatch`) blocks
bot actors by default, so dispatched reviews previously failed with "Workflow
initiated by non-human actor" — and, having entered the per-PR concurrency
group, canceled the parallel `synchronize` auto-review on their way out,
leaving the push with no review at all.
- `claude-code-review`'s "collapse previous review comments" step is no longer
gated to `pull_request`, so a dispatched (`workflow_dispatch`) review that
wins the per-PR concurrency race also folds earlier pushes' review comments as
OUTDATED instead of leaving them expanded.
### Security
- **All third-party actions are now pinned to full commit SHAs** (with the
human-readable version in a trailing comment), following GitHub's
[recommended hardening posture](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-third-party-actions).
A SHA is immutable, so a re-pointed tag or a compromised upstream can no longer
silently change what runs — most important for the `preview-deploy` job, which
runs in the base-repo context with `contents: write` + `pull-requests: write`.
Added [`.github/dependabot.yml`](.github/dependabot.yml) (`github-actions`
ecosystem, weekly, grouped, covering `.github/workflows/` and each composite
action) so the pins are auto-bumped as upstreams publish releases instead of
freezing. First-party `d-morrison/gha/*@v1` self-references and the
`examples/` templates intentionally still track the `@v1` major tag (#48).
## [v1] — initial pilot set
Reusable workflows + composite actions:
- `check-bibliography-dois` — validate book/article BibTeX entries have
resolvable DOIs matching CrossRef metadata.
- `check-links` — lychee link check with bundled config, PR skip-label, and
auto-issue on `main`.
- `check-non-standard-chars` — detect curly quotes / en–em dashes in `.qmd`
and `.R` files.
- `check-news` — enforce a `NEWS.md` changelog entry on PRs (wraps
`UCD-SERG/changelog-check-action`).
- `summary` — AI summary comment on newly opened issues.
[Unreleased]: https://github.com/d-morrison/gha/compare/v1...main
[v1]: https://github.com/d-morrison/gha/releases/tag/v1
# CLAUDE.md
Guidance for Claude Code when working in this repository.
## About this repo
Central, reusable GitHub Actions for `d-morrison` / `UCD-SERG` / `ucdavis` R-package
and Quarto repositories (see [`README.md`](README.md)). Each capability ships as a
composite action plus a `workflow_call` reusable workflow. Consumers pin to `@v1`.
### Layout
- Per-capability composite-action directories at the repo root, each with an
`action.yml` and, for R/Python capabilities, a language-specific helper
script — e.g. `check-bibliography-dois/` (R), `check-non-standard-chars/` and
`check-phi/` (Python). `check-links/` bundles `lychee.default.toml`;
`preview/`, `quarto-publish/`, and `open-sync-pr/` are action-only (the last
is the shared push-and-open-PR helper used by `bump-submodule` and
`sync-shared-fragments`).
- `.github/workflows/` — the `workflow_call` reusable workflows that wrap the
composites (one per consumer-facing capability — the shared internal
`open-sync-pr` composite has no wrapper), plus the `claude.yml` and
`claude-code-review.yml` reusable wrappers, and `_selftest.yml`, which
exercises composites on every PR — local `./` refs for pre-release
capabilities, and `@v1` through the reusable-workflow wrappers for stable ones.
`claude-bot.yml` and `claude-review.yml` are event-triggered workflows that run
the Claude bot in this repo, not `workflow_call` wrappers.
- Several workflows have no corresponding root composite: `check-news.yml`,
`summary.yml`, and `preview-deploy.yml` are `workflow_call` reusable workflows
that wrap external actions; `cleanup-pr-previews.yml` is a self-contained
`workflow_call` reusable workflow (inline shell logic, no external composite);
`bump-submodule.yml` and `sync-shared-fragments.yml` are `workflow_call`
reusable workflows that call the shared internal `open-sync-pr` composite;
`slide-major-tag.yml` is push- and dispatch-triggered and runs only in this
repo.
- `.github/actions/checkout-submodules/` — a small shared composite reused by the
reusable workflows.
- `examples/` — caller stubs consumers copy into their own repos.
- `README.md`, `CHANGELOG.md` — top-level project docs;
`REVDEPS.md` — lists registered downstream consumer repos. Every PR that
changes user-facing behavior should add a **changelog fragment** under
`changelog.d/` (a `<slug>.<category>.md` file — see `changelog.d/README.md`)
rather than editing `CHANGELOG.md` directly, so parallel PRs never conflict on
the same `## [Unreleased]` lines. `changelog.d/assemble.sh` collates the
fragments into `CHANGELOG.md` at release time. This is not CI-enforced
(`require-changelog.yml` was removed).
When editing a consumer-facing capability, change the composite (`<name>/action.yml`,
plus its helper script if one exists) and keep the wrapping reusable workflow and its
`examples/<name>.yml` stub in sync. Internal-only composites (like `open-sync-pr`)
have no wrapper or example stub to update. New `.github/workflows/` changes are
exercised by `_selftest.yml`; because brand-new actions aren't at the `@v1` tag
yet, the selftest runs them via the local `./<name>` ref until release.
**A brand-new capability that ships at a tag newer than `@v1`** (because `@v1`
was frozen before it existed — see `slide-major-tag.yml` / the Versioning
section of `README.md`) needs its major tag updated at two distinct kinds of
site, not just the obvious one:
1. **Capability-specific refs** — the new capability's own caller stub
(`examples/<name>.yml`) and reference-page example
(`website/reference/<name>.qmd`).
2. **Blanket-rule prose** — any general "pin every reference to `@v1`"
statement elsewhere (`README.md`'s Versioning section,
`website/workflows.qmd`) needs an exception clause, even though it never
names the new capability.
Grep the repo for `@v1` rather than relying on memory of where it appears.
Missing either kind surfaces as a workflow-not-found error for consumers who
copy that spot literally (gha#148, caught across two review rounds).
**Adding a new `workflow_call` input to an existing reusable workflow** needs
its own doc sync at three sites beyond the workflow file itself, or the input
is invisible to a consumer skimming the docs:
1. **`README.md`**'s per-workflow table row's "Key inputs" cell.
2. **`website/workflows.qmd`**'s equivalent table row — a separate table, not
generated from `README.md`, so it drifts independently.
3. **`website/reference/<name>.qmd`**'s Inputs table, plus a commented usage
line in its `## Example` block.
Grep the repo for the workflow's filename (e.g. `claude-code-review.yml`)
across `README.md`, `website/workflows.qmd`, and `website/reference/` rather
than assuming only one needs the update. Caught across four review rounds on
gha#161 — the fix for round 2's finding (missing composite) surfaced round
3's finding (docs out of sync), whose fix left one more untouched table row
that round 3 flagged as out-of-scope, fixed anyway before round 4 confirmed
clean.
### Tests
`check-phi/tests/test_detectors.py` is a pytest suite pinning each PHI detector's
positive and negative behavior. Run it with `python3 -m pytest check-phi/tests/ -q`;
CI runs it as the `phi-tests` job in `_selftest.yml`. There's no broader unit-test
harness — most capabilities are validated end-to-end by `_selftest.yml`, running
against this repo itself or small throwaway fixtures (stable capabilities via
`@v1`, pre-release ones from local source).
**Generate selftest fixtures at runtime; don't commit them.** A fixture
committed under a composite's `tests/` dir (e.g. a minimal R package for
`test-coverage`) gets swept into OTHER selftest jobs' repo-wide scans: the
`bib` job's dependency resolution tries to treat it as a real package, and the
`phi` job's PHI scanner flags any synthetic identifier in it (a fake
maintainer email, etc.). Generate the fixture in a small script
(`test-coverage/tests/make-fixture.sh` is the pattern) that the `coverage`
selftest job runs before invoking the composite, instead of committing R
package source files (gha#148).
## GitHub access in remote / web sessions
Claude Code on the web (and other remote/CI sessions) runs in a sandbox where the
`gh` and `glab` CLIs are **not installed** and there is no direct GitHub API
access. Skills and built-in commands that tell you to "use `gh`" — `/review`,
`/code-review --comment`, `/security-review`, `/verify`, PR babysitting, PR
creation — only work if their GitHub steps are translated to the GitHub MCP tools
(`mcp__github__*`). When a skill or command instructs a `gh`/`glab` command in
such a session, substitute the equivalent MCP tool below. (In a local session
where `gh` is on `PATH`, use `gh` as the skill describes.)
This repo is `d-morrison/gha`, so MCP calls use `owner: d-morrison`, `repo: gha`.
**Some of these sessions have no local git checkout at all** (not just a missing
`gh` CLI) — there is no working tree to run `git commit`/`git push` against, so
every change (branch, file edit, PR) must go through the MCP write tools below.
Editing a file means: `mcp__github__get_file_contents` first to get its current
blob `sha` (required on every update, not just the first — re-fetch it after
each write since it changes on every commit), then
`mcp__github__create_or_update_file` with the **full** new file content (it
replaces the whole file, there is no patch/diff mode) and that `sha`. A stale
`sha` (from before another commit landed) fails the write — re-fetch and retry
rather than guessing.
| Operation / `gh`/`glab` command | GitHub MCP equivalent |
| --- | --- |
| `gh pr list` | `mcp__github__list_pull_requests` |
| `gh pr view <n>` | `mcp__github__pull_request_read` (`method: get`) |
| `gh pr diff <n>` | `mcp__github__pull_request_read` (`method: get_diff`) |
| changed files in a PR | `mcp__github__pull_request_read` (`method: get_files`) |
| `gh pr status` / `gh pr checks` | `mcp__github__pull_request_read` (`method: get_status` / `get_check_runs`) |
| `gh pr create` | `mcp__github__create_pull_request` |
| read PR conversation comments | `mcp__github__pull_request_read` (`method: get_comments`) |
| read inline review comments | `mcp__github__pull_request_read` (`method: get_review_comments`) — also returns `threadId`s |
| post a top-level PR comment | `mcp__github__add_issue_comment` |
| post inline review comments | `mcp__github__pull_request_review_write` (`method: create`, no `event`) → `mcp__github__add_comment_to_pending_review` per comment → `mcp__github__pull_request_review_write` (`method: submit_pending`) |
| reply to a review comment | `mcp__github__add_reply_to_pull_request_comment` |
| approve / request changes | `mcp__github__pull_request_review_write` (`method: create` with `event`) |
| resolve a review thread | `mcp__github__pull_request_review_write` (`method: resolve_thread`, `threadId: <id from get_review_comments>`) |
| `gh issue list` / `gh issue view <n>` | `mcp__github__list_issues` / `mcp__github__issue_read` |
| read a file / repo contents | `mcp__github__get_file_contents` |
| create/edit a file (no local checkout) | `mcp__github__create_or_update_file` — needs the target branch, full new file content, and the file's current blob `sha` (from `get_file_contents`) if it already exists |
| create a branch (no local checkout) | `mcp__github__create_branch` |
| CI runs & job logs | `mcp__github__actions_list`, `mcp__github__actions_get`, `mcp__github__get_job_logs` |
| watch / stop watching PR activity | `mcp__github__subscribe_pr_activity` / `mcp__github__unsubscribe_pr_activity` |
| `glab mr ...` (GitLab) | N/A — this repo is on GitHub; use the tools above |
Posting inline comments requires a **pending review to already exist** before
`mcp__github__add_comment_to_pending_review`; create the pending review first, add
each comment, then submit once at the end. Watch and respond to PR activity with
`mcp__github__subscribe_pr_activity` / `mcp__github__unsubscribe_pr_activity` (not
`gh pr checks --watch`).
### Reading repos outside the session's MCP scope
A task often needs files from a *sibling* repo (e.g. `d-morrison/qwt`) that the
session's GitHub MCP tools aren't scoped to — those calls fail with
`Access denied: repository … is not configured for this session`. **Don't report
the repo as inaccessible from that alone.** First try the raw HTTP URL directly:
any **public** repo's files are fetchable with `curl` (or `WebFetch`) at
`https://raw.githubusercontent.com/<owner>/<repo>/<branch>/<path>`, which works
even when `gh` and the MCP tools don't. (This is how qwt's standalone workflows
were obtained to port them faithfully into the reusable workflows for #44/#45.)
Only fall back to "can't access it" — or to whatever session tooling can add a
repo to scope, if any — after the raw fetch also fails (private repo, or the
network policy blocks the host).
**A 403 from a *rendered* docs site is not the same as the content being
inaccessible.** A GitHub Pages / Quarto-rendered site (e.g.
`ucd-serg.github.io/lab-manual/coding-style.html`) can reject `WebFetch` (for
reasons unclear — possibly anti-scraping) even though the *source* file it
was built from is a plain file in a public repo. Don't conclude the content is
unreachable — find the source path (often the same repo, e.g.
`coding-style.qmd` for `coding-style.html`, sometimes with `_`-prefixed
included fragments) and raw-fetch that instead using the same
`<path>`-includes-its-extension template above, e.g.
`https://raw.githubusercontent.com/<owner>/<repo>/<branch>/coding-style.qmd`.
(Used on `ucdavis/ettbc#12` / `#32` to confirm ettbc's `.lintr.R` predated
`UCD-SERG/lab-manual`'s move to a shared `lms` linter package (source:
[`UCD-SERG/lab-manual/.lintr.R`](https://github.com/UCD-SERG/lab-manual/blob/main/.lintr.R),
which calls `lms::default_linters()` from a package defined in that repo's own
`lms/` subdirectory) — the manual's own docs page 403'd, but its `.qmd`
source and the referenced `.lintr.R` file both fetched cleanly.)
## A canceled review can red-X require-review — don't chase it as a code bug
`claude-code-review.yml`'s `claude-review` job is concurrency-grouped per PR
(`claude-review-<PR>`, `cancel-in-progress: true`) across BOTH the automatic
`pull_request`-triggered review and claude.yml's comment-triggered (`@claude
review`) re-dispatch. When a push and an `@claude review` comment land close
together — or claude.yml's agent run finishes and re-dispatches a review a
minute or two later, landing on top of the next push's auto-review — the two
reviews race and one cancels the other.
The `require-review` gate job asserts `claude-review`'s result is `success`;
a *canceled* run (not skipped) makes that assertion fail, so `require-review`
shows red right after a push even though the surviving review is fine. Before
treating a post-push `require-review` failure as a real problem: check
whether `claude-review`'s conclusion is `cancelled` rather than `failure`. If
so, it's this race, not a code issue — wait for (or re-trigger) an
uncontested review instead of debugging the diff. To avoid causing it: don't
post `@claude review` immediately after pushing a commit on a PR using this
workflow; let the automatic review run alone, or wait for any in-flight
dispatched review to finish first. (See the `claude-review` job's
`concurrency:` comment in `.github/workflows/claude-code-review.yml` for the
full mechanism.)
## Code review guidelines
When reviewing a pull request (e.g. via `/review`, `/code-review`, or as a Claude
PR bot), evaluate the diff against **both** of the following, in addition to
correctness:
### 1. The SERG lab manual
The [UCD-SERG lab manual](https://ucd-serg.github.io/lab-manual/) is the lab's
authority on coding conventions. Hold changes to its standards, especially:
- [Coding style](https://ucd-serg.github.io/lab-manual/coding-style.html) —
object naming, line breaks/formatting, function documentation, comments,
message/communication style, and Quarto code-reference conventions (backticked
`pkg::fn()`, markdown package links — no raw HTML in `.qmd`).
- [Coding practices](https://ucd-serg.github.io/lab-manual/coding-practices.html) —
function decomposition and length limits, testing requirements, the QA
checklist, documentation, `{here}` for paths, and tidyverse idioms.
- [Code repositories](https://ucd-serg.github.io/lab-manual/code-repositories.html) —
repository organization and version-control practices.
The manual defers to the [tidyverse style guide](https://style.tidyverse.org/)
for R; prefer tidyverse idioms and the native `|>` pipe.
### 2. d-morrison's review priorities
Above all, code should be **highly modular and idiomatic**:
- **Modular / decomposed.** Favor small, single-purpose functions over long
monolithic blocks. Flag duplicated logic (DRY), functions that do too much,
deep nesting, and steps that should be extracted and named. In workflows and
composite actions, factor shared logic into reusable units rather than copying
it between files.
- **Idiomatic.** Code should read like the surrounding code and like the
ecosystem's conventions — idiomatic R (tidyverse), idiomatic YAML/GitHub
Actions, idiomatic shell. Prefer the standard, well-known way over a clever or
bespoke one. Match existing naming, structure, and formatting in the file.
- Keep these front-of-mind: surface modularity and idiom issues even when the
code is otherwise correct.
Be specific and cite the relevant manual section or principle when raising a
point. Distinguish blocking issues from optional suggestions.
### 3. Challenge ambiguous phrasing and terminology
Flag ambiguous terms and phrasing rather than accepting a plausible-sounding
reading — a name that could mean more than one thing, a claim that cites a
value or construct without confirming it exists in the actual code. This is a
global standing rule from the
[`d-morrison/ai-config`](https://github.com/d-morrison/ai-config) corpus.
Ambiguity accepted at face value is how a factually wrong claim (e.g.
documentation citing a nonexistent enum value) slips through review
unchallenged.
### 4. Fact-check prose against domain knowledge and external sources
When a diff touches prose (`README.md`, `CHANGELOG.md`, `website/`, action
descriptions), assess the accuracy and clarity of its claims — check each
against domain knowledge and, where checkable, an external source (the
referenced tool's own docs, a linked spec) — and check any document-internal
reasoning the prose makes (e.g. a justification for why a workflow does
something a particular way). State which claims are inaccurate, cite the
specific source checked for each judgment, and proactively suggest
additional citations where they'd help. This is a global standing rule from
the [`d-morrison/ai-config`](https://github.com/d-morrison/ai-config) corpus
(`shared/writing/fact-check-prose.md`).
MIT License
Copyright (c) 2026 Douglas Ezra Morrison
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# d-morrison/gha
Central, reusable GitHub Actions for d-morrison / UCD-SERG / ucdavis R-package
and Quarto repositories. Modeled on
[`r-lib/actions`](https://github.com/r-lib/actions) and
[`easystats/workflows`](https://github.com/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.
## How it works
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 as
`d-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/`](examples)):
```yaml
name: Check Bibliography DOIs
on:
push: { branches: [main] }
pull_request:
workflow_dispatch:
jobs:
check:
uses: d-morrison/gha/.github/workflows/check-bibliography-dois.yml@v1
```
Pin to `@v1` (a moving major tag updated as fixes land). Do not reference
`@main` from consumers.
## Available reusable workflows
| 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`, `fail`, `fail-if-empty`, `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` |
| `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` |
| `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 when `track-progress: true`, consolidated summary otherwise) | `pr-number`, `prompt-addendum`, `checkout-submodules`, `allowed-bots`, `track-progress`, `apt-packages`, `pip-packages` |
| `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) | `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` |
| `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` |
## Permissions
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 on `main` failures) → grant `issues: write`,
`pull-requests: read`, `contents: read`.
- `summary` (comments on issues, calls the models API) → grant `issues: write`,
`models: read`, `contents: read`.
- `check-bibliography-dois`, `check-non-standard-chars`, `check-phi`,
`test-coverage` → only `contents: read` (the default), so no `permissions:`
block is needed. `test-coverage` additionally takes an optional
`CODECOV_TOKEN` secret, passed through the caller's `secrets:` block.
- `quarto-publish` (deploys to the `gh-pages` branch, which Pages serves) →
grant `contents: write`, and set Settings → Pages → Source = "Deploy from a
branch", branch `gh-pages` / `(root)` once. Grant `contents: write` even with
`deploy: false` — the deploy job is part of the workflow, so the caller must
grant its permissions even when it is skipped.
- `claude` (pushes branches, opens PRs, dispatches the review workflow) → grant
`contents: write`, `pull-requests: write`, `issues: write`, `id-token: write`,
`actions: write`, and add the `CLAUDE_CODE_OAUTH_TOKEN` secret.
- **Optional:** if Claude will edit files under `.github/workflows/`, also add
a `WORKFLOW_TOKEN` secret (a PAT or GitHub App token with `contents:write` +
`workflows:write`). The integrated `GITHUB_TOKEN` cannot push workflow-file
changes — GitHub rejects them without the `workflows` scope. Repos that never
touch `.github/workflows/` can omit it; pushes fall back to `GITHUB_TOKEN`.
Note that, unlike `GITHUB_TOKEN`, a PAT/App-token push **does** trigger other
`push`-based workflows, so enabling `WORKFLOW_TOKEN` can set off extra CI runs.
- **Optional:** set `checkout-submodules: true` so Claude can read submodule
contents. Public submodules clone anonymously; private ones additionally need
a `SUBMODULES_TOKEN` secret.
- `claude-code-review` (read-only review) → grant `contents: read`,
`pull-requests: write`, `issues: write`, `id-token: write`, and the
`CLAUDE_CODE_OAUTH_TOKEN` secret.
- **Optional:** set `checkout-submodules: true` so the reviewer can read
submodule contents instead of reporting them as uninitialized. Public
submodules clone anonymously; private ones additionally need a
`SUBMODULES_TOKEN` secret.
- `preview` (build half, read-only) → only `contents: read` (the default).
- `preview-deploy` (deploy half, pushes `gh-pages` + comments) → grant
`contents: write`, `pull-requests: write`, `actions: read`.
- `check-equation-renders` (downloads the build artifact, read-only) → grant
`contents: read`, `actions: read`.
- `cleanup-pr-previews` (commits deletions to `gh-pages`) → grant
`contents: write`, `pull-requests: read`.
- `bump-submodule`, `sync-shared-fragments` (open a PR) → grant `contents: write`,
`pull-requests: write`, and enable Settings → Actions → General → "Allow
GitHub Actions to create and approve pull requests" so the integrated
`GITHUB_TOKEN` can open the PR. For private submodules, `bump-submodule` also
needs a `SUBMODULES_TOKEN` secret. Add a `WORKFLOW_TOKEN` only to push to a
protected branch; otherwise pushes fall back to `GITHUB_TOKEN`.
The stubs in [`examples/`](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.
You can also start a review **directly**, without waking the `@claude` agent, by
commenting `/review` at the start of a PR comment. `claude-code-review.yml`
listens for that comment itself and re-dispatches its own `workflow_dispatch`
review of the PR — so `/review` needs only `claude-code-review.yml` installed
(no `claude.yml`), 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.
## Claude session visibility
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](https://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`. |
## PHI scanning (`check-phi`)
`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. `push` runs 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:col` and 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-allow`
comment on the line, or list a regex matching the value in an allowlist file
(defaults to `.github/phi-allowlist.txt` when present; override with the
`allowlist-file` input). Use `fail: false` to downgrade to warnings.
## PR previews (`preview` family)
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/`](examples):
1. **`preview.yml`** (build) — triggered on `pull_request`. Renders the site and
uploads it plus the PR metadata as a `pr-preview-site` artifact. Runs
**read-only** in the (possibly fork) PR context, so it can't write to the
base repo.
2. **`preview-deploy.yml`** (deploy) — triggered on `workflow_run` completion of
the build. Downloads the artifact and publishes it to `gh-pages` in the
**base-repo** context (where the token can write), then comments the preview
link on the PR.
3. **`check-equation-renders.yml`** — also triggered on `workflow_run`
completion 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 (no `gh-pages` write needed),
not sequenced after it.
4. **`cleanup-pr-previews.yml`** (housekeeping) — scheduled. Removes preview
directories for PRs that have closed. Set `compact-history: true` to also
orphan-squash `gh-pages` to 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's `name:`** (all default to `Quarto Preview
Build` in the examples). That string is how `workflow_run` finds the build.
- `workflow_run` and `schedule` triggers 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 to `main`.
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.
## Shared-content sync (`bump-submodule` + `sync-shared-fragments`)
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 by `UCD-SERG/lab-manual`, which carries
`d-morrison/ai-config` as `.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 a `dest-dir`,
records the source repo and commit in a JSON manifest, and opens a PR when the
copy changes. (Used by `d-morrison/ai-config` to 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.
## Versioning
Releases are tagged `vX.Y.Z`; the `vX` major tag moves to the latest compatible
release. Consumers reference `@v1`, except `test-coverage.yml` and
`check-equation-renders.yml`, which ship at `@v2` (too new for the frozen
`@v1` tag). See [`CHANGELOG.md`](CHANGELOG.md) for
what changes as a major tag moves and for any breaking-change migration steps.
Changelog entries are added as fragment files under
[`changelog.d/`](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`](changelog.d/README.md).
### Pinning third-party actions
Every **third-party** action is pinned to a full commit SHA, with the
human-readable version in a trailing comment, e.g.:
```yaml
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
```
This is GitHub's [recommended hardening posture](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-third-party-actions):
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`](.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/`](examples/)
templates intentionally track the `@v1` major tag (so consumers ride the moving
major), and so are **not** SHA-pinned.
## Reverse dependencies
[`REVDEPS.md`](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.
## Notes for private consumers
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.
## Scope
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.
# Reverse Dependencies (Consumer Repos)
Repos that call `d-morrison/gha` reusable workflows from their
`.github/workflows/`.
> **Note:** This list helps us notify consumers before moving the `@v1` tag in
> a breaking way (or cutting `@v2`). It is **not** authoritative — always
> verify with a code search across the consuming orgs (`d-morrison`,
> `ucdavis`, `UCD-SERG`, `UCLA-PHP`, `UCD-IDDRC`) when releasing a breaking
> change. A GitHub code search for `d-morrison/gha/.github/workflows` across
> those owners is the quickest way to find current callers:
>
> ```bash
> # Requires an authenticated gh (run `gh auth login`, or set GH_TOKEN).
> gh search code 'uses: d-morrison/gha/.github/workflows' --owner d-morrison --owner ucdavis --owner UCD-SERG --owner UCLA-PHP --owner UCD-IDDRC
> ```
## How to register
If your repo calls a `gha` workflow, please open a PR adding it below (or file
an issue asking to be added). Similarly, if you stop using `gha`, open a PR or
issue to be removed.
## Consumer list
| Repo | Workflows used | Notes |
|------|----------------|-------|
| [`d-morrison/qwt`](https://github.com/d-morrison/qwt) | `check-bibliography-dois`, `check-non-standard-chars`, `check-links` | Quarto website template (propagates to downstream books via "Use this template"). Phase 1 migration ([qwt#115](https://github.com/d-morrison/qwt/pull/115)); `summary` + the Claude workflows pending parity ([qwt#116](https://github.com/d-morrison/qwt/issues/116)). |
| [`d-morrison/rme`](https://github.com/d-morrison/rme) | `preview`, `preview-deploy`, `cleanup-pr-previews` | The original motivation for the PR-preview family (see [#33](https://github.com/d-morrison/gha/issues/33)/[#34](https://github.com/d-morrison/gha/pull/34)). Migrated its three inlined preview workflows to the gha family in [rme#942](https://github.com/d-morrison/rme/pull/942) ([#75](https://github.com/d-morrison/gha/issues/75)). |
| [`Lacaedemon/sparta`](https://github.com/Lacaedemon/sparta) | `check-links`, `claude`, `claude-code-review`, `summary`, `quarto-publish` | Godot game; docs site published via `quarto-publish` (injects recorded gameplay clips through `pre-render-artifact`). |
Summary
track_progress: true) inclaude-code-actionhardcodes git write tools (Bash(git add:*),Bash(git commit:*),Bash(git rm:*),git-push.sh) intoALLOWED_TOOLSvia the action's TypeScript, regardless of--disallowedToolsinclaude_args. The CLI flag cannot override an env var set internally by the action.02af72b, was pushed by workflow run28271356338during what was supposed to be a read-only review triggered by apull_requestsynchronize event.track_progress: 'false'for all events. In agent mode,ALLOWED_TOOLSis built solely fromclaude_args, so no git write tools are ever available.Trade-offs and mitigations
Agent mode does not auto-post a tracking comment or initialize the inline-comment tool (claude-code-action #635). To compensate:
workflow_dispatch-only) now runs on all events, sopull_requestreviews still appear on the PR as top-level comments with summary-format findings.The result is a regression from inline comments to summary-only reviews on
pull_requestevents. That's the correct trade-off: a read-only reviewer that posts summary comments is strictly better than one that can push commits to the branch it's reviewing.Upstream fix
This is a workaround. The proper fix is a
read_onlymode inclaude-code-actionitself — tracked in anthropics/claude-code-action#1415, with draft implementation in anthropics/claude-code-action#1433. Once that lands and the action is updated here, we can restore tag mode (and inline comments) by passingread_only: trueinstead oftrack_progress: false.