Skip to content

Commit b4fdcdb

Browse files
d-morrisonclaude
andcommitted
Migrate agent and publish workflows to Morrison-Lab/gha
All three files were stock hand-rolled templates. Replace them with caller stubs targeting gha's reusable workflows at @v2. claude.yml gains a trusted-author gate (the stock template fired on any @claude mention from anyone) and write permissions, so the agent can push a branch and open a PR rather than only comment. setup-r is turned off: it defaults on, and with use-renv false it resolves local::. against a DESCRIPTION this repo does not have. claude-code-review.yml drops the pull_request trigger in favour of gha's dispatch path. claude.yml re-dispatches a review after the agent pushes, so an agent PR is still reviewed automatically; keeping a synchronize trigger alongside that would race it, and the reusable workflow's cancel-in-progress concurrency would cancel one of the two. Human PRs review via /review. publish.yml serializes gh-pages deploys, names its R dependencies directly (no renv lockfile, no DESCRIPTION), drops the unused tinytex install, and files an issue when a deploy fails. Secrets are passed explicitly in every stub: this repo is d-morrison-owned and gha is Morrison-Lab-owned, so secrets: inherit would yield an empty token. preview.yml is left alone. gha's preview composite has no r-packages input, so a Quarto site that is neither an renv project nor an R package cannot use it; tracked as Morrison-Lab/gha#607 and #82. Refs #81 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DzqQ9SB4Uw9ykenZ8xmPZM
1 parent d919f59 commit b4fdcdb

3 files changed

Lines changed: 172 additions & 100 deletions

File tree

Lines changed: 109 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,121 @@
1+
# PR review, delegated to Morrison-Lab/gha's reusable workflow.
2+
#
3+
# Do NOT add a top-level `concurrency:` block. The reusable workflow manages
4+
# per-PR concurrency on its own job; a PR-scoped group here deadlocks Actions
5+
# against the nested job and cancels the run (gha#437).
6+
#
7+
# Secrets are passed explicitly rather than via `secrets: inherit`, since this
8+
# repo is d-morrison-owned and gha is Morrison-Lab-owned; inheritance across
9+
# owners yields an empty token.
10+
#
11+
# There is deliberately no `pull_request:` trigger. claude.yml re-dispatches
12+
# this workflow after the agent pushes commits, so an agent-authored PR is
13+
# reviewed automatically. Adding a synchronize trigger on top would race that
14+
# dispatch, and the reusable workflow's cancel-in-progress concurrency would
15+
# cancel one of the two. A human-authored PR gets a review via `/review` or an
16+
# `@claude review` mention.
117
name: Claude Code Review
218

319
on:
4-
pull_request:
5-
types: [opened, synchronize, ready_for_review, reopened]
6-
# Optional: Only run on specific file changes
7-
# paths:
8-
# - "src/**/*.ts"
9-
# - "src/**/*.tsx"
10-
# - "src/**/*.js"
11-
# - "src/**/*.jsx"
20+
issue_comment:
21+
types: [created]
22+
workflow_dispatch:
23+
inputs:
24+
pr_number:
25+
description: 'Pull request number to review'
26+
required: true
27+
type: string
1228

1329
jobs:
14-
claude-review:
15-
# Optional: Filter by PR author
16-
# if: |
17-
# github.event.pull_request.user.login == 'external-contributor' ||
18-
# github.event.pull_request.user.login == 'new-developer' ||
19-
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20-
30+
# `/review` at the start of a PR comment from a trusted author dispatches an
31+
# on-demand review, re-entering via the workflow_dispatch path below.
32+
dispatch-on-comment:
33+
if: >-
34+
github.event_name == 'issue_comment' &&
35+
github.event.issue.pull_request &&
36+
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) &&
37+
(startsWith(github.event.comment.body, '/review') ||
38+
(vars.CLAUDE_AGENT_DISABLED == 'true' &&
39+
contains(github.event.comment.body, '@claude')))
2140
runs-on: ubuntu-latest
2241
permissions:
23-
contents: read
24-
pull-requests: read
25-
issues: read
26-
id-token: write
27-
42+
actions: write # dispatch this workflow via `gh workflow run`
43+
issues: write # acknowledge the /review comment
2844
steps:
29-
- name: Checkout repository
30-
uses: actions/checkout@v4
45+
- name: Parse this workflow's ref
46+
id: this-wf
47+
uses: Morrison-Lab/gha/.github/actions/parse-workflow-ref@v2
3148
with:
32-
fetch-depth: 1
49+
workflow-ref: ${{ github.workflow_ref }}
3350

34-
- name: Run Claude Code Review
35-
id: claude-review
36-
uses: anthropics/claude-code-action@v1
51+
# Only runs when the agent has been switched off repo-wide; with
52+
# claude.yml live, `/review` is the sole comment trigger, so the two
53+
# workflows never answer the same comment with two paid review runs.
54+
- name: Check for an `@claude review` request
55+
id: mention
56+
if: ${{ vars.CLAUDE_AGENT_DISABLED == 'true' }}
57+
uses: Morrison-Lab/gha/.github/actions/detect-review-request@v2
3758
with:
38-
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39-
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
40-
plugins: 'code-review@claude-code-plugins'
41-
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
42-
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
43-
# or https://code.claude.com/docs/en/cli-reference for available options
59+
comment-body: ${{ github.event.comment.body }}
4460

61+
- name: Dispatch a review for the commented PR
62+
env:
63+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
64+
REPO: ${{ github.repository }}
65+
PR_NUMBER: ${{ github.event.issue.number }}
66+
WF_PATH: ${{ steps.this-wf.outputs.path }}
67+
# Passed via env, never inlined, so the comment body is a shell value
68+
# rather than script text.
69+
COMMENT_BODY: ${{ github.event.comment.body }}
70+
MENTION_MATCH: ${{ steps.mention.outputs.match }}
71+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
72+
run: |
73+
set -euo pipefail
74+
# The job-level `if:` can only pre-filter cheaply: Actions expressions
75+
# cannot tell '/review' from '/reviewer', nor handle a newline after
76+
# the command. Enforce the real match here.
77+
if [[ "$COMMENT_BODY" =~ ^/review([[:space:]]|$) ]]; then
78+
TRIGGER='/review'
79+
elif [ "$MENTION_MATCH" = 'true' ]; then
80+
TRIGGER='@claude review'
81+
else
82+
echo "Comment is neither a standalone '/review' command nor an '@claude review' request; skipping dispatch."
83+
exit 0
84+
fi
85+
WF_FILE=$(basename "$WF_PATH")
86+
# This job never checks out the repo, so the PR's head branch has to
87+
# be looked up. --ref pins the dispatched run's check-runs to that
88+
# branch rather than the default branch workflow_dispatch would
89+
# otherwise fall back to.
90+
PR_JSON=$(gh api "repos/$REPO/pulls/$PR_NUMBER")
91+
PR_BRANCH=$(jq -r '.head.ref' <<< "$PR_JSON")
92+
PR_HEAD_REPO=$(jq -r '.head.repo.full_name' <<< "$PR_JSON")
93+
echo "Dispatching $WF_FILE to review PR #$PR_NUMBER ($PR_BRANCH) ($TRIGGER comment)."
94+
# A fork PR's head branch exists only in the fork, so --ref cannot
95+
# resolve there; drop it and accept the default-branch check-run
96+
# attribution, which is the lesser problem.
97+
REF_ARGS=(--ref "$PR_BRANCH")
98+
if [ "$PR_HEAD_REPO" != "$REPO" ]; then
99+
echo "::notice::PR #$PR_NUMBER is from a fork ($PR_HEAD_REPO); dispatching $WF_FILE without --ref."
100+
REF_ARGS=()
101+
fi
102+
gh workflow run "$WF_FILE" --repo "$REPO" "${REF_ARGS[@]}" -f pr_number="$PR_NUMBER"
103+
gh issue comment "$PR_NUMBER" --repo "$REPO" \
104+
--body ":mag: \`$TRIGGER\` received -- dispatched a Claude review of this PR (see the [dispatch run]($RUN_URL)). The review posts as its own comment when it finishes." \
105+
|| echo "::warning::Could not acknowledge the $TRIGGER comment."
106+
107+
review:
108+
# workflow_dispatch only; the issue_comment path re-enters through
109+
# dispatch-on-comment above.
110+
if: github.event_name != 'issue_comment'
111+
permissions:
112+
contents: read
113+
pull-requests: write
114+
issues: write
115+
id-token: write
116+
actions: read # lets the reviewer read CI status (github_ci MCP server)
117+
uses: Morrison-Lab/gha/.github/workflows/claude-code-review.yml@v2
118+
secrets:
119+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
120+
with:
121+
pr-number: ${{ inputs.pr_number }}

.github/workflows/claude.yml

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# The @claude agent, delegated to Morrison-Lab/gha's reusable workflow.
2+
#
3+
# Secrets are passed explicitly rather than via `secrets: inherit`. GitHub only
4+
# inherits secrets into a reusable workflow owned by the same org/user, and this
5+
# repo is d-morrison-owned while gha is Morrison-Lab-owned, so inheritance would
6+
# hand the workflow an empty token.
17
name: Claude Code
28

39
on:
@@ -12,39 +18,28 @@ on:
1218

1319
jobs:
1420
claude:
21+
# Caller-side gate: require both an @claude mention and a trusted author, so
22+
# an untrusted commenter's mention never reaches a workflow holding elevated
23+
# permissions and secrets. The reusable workflow gates again on its own side.
1524
if: |
16-
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17-
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18-
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19-
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20-
runs-on: ubuntu-latest
25+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) ||
26+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) ||
27+
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.review.author_association)) ||
28+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association))
2129
permissions:
22-
contents: read
23-
pull-requests: read
24-
issues: read
30+
contents: write
31+
pull-requests: write
32+
issues: write
2533
id-token: write
26-
actions: read # Required for Claude to read CI results on PRs
27-
steps:
28-
- name: Checkout repository
29-
uses: actions/checkout@v4
30-
with:
31-
fetch-depth: 1
32-
33-
- name: Run Claude Code
34-
id: claude
35-
uses: anthropics/claude-code-action@v1
36-
with:
37-
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38-
39-
# This is an optional setting that allows Claude to read CI results on PRs
40-
additional_permissions: |
41-
actions: read
42-
43-
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
44-
# prompt: 'Update the pull request description to include a summary of changes.'
45-
46-
# Optional: Add claude_args to customize behavior and configuration
47-
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
48-
# or https://code.claude.com/docs/en/cli-reference for available options
49-
# claude_args: '--allowed-tools Bash(gh pr *)'
50-
34+
actions: write # dispatch the review workflow via `gh workflow run`
35+
uses: Morrison-Lab/gha/.github/workflows/claude.yml@v2
36+
secrets:
37+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38+
with:
39+
# setup-r defaults to true, and with use-renv false it runs
40+
# setup-r-dependencies, whose `local::.` spec resolves against a
41+
# DESCRIPTION file. This repo is a Quarto site, not an R package, so it
42+
# has no DESCRIPTION and that step would fail. Rendering is CI's job
43+
# (publish.yml / preview.yml); the agent edits macros.qmd and
44+
# interpretations.tsv, neither of which needs R on the runner.
45+
setup-r: false

.github/workflows/publish.yml

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,43 @@
1+
# Publish the site to GitHub Pages on `main`, delegated to Morrison-Lab/gha's
2+
# reusable Quarto publish workflow. The deploy preserves the pr-preview/
3+
# subtree on gh-pages, so open PRs' previews survive a publish.
4+
name: Quarto Publish
5+
16
on:
2-
workflow_dispatch:
37
push:
4-
branches: main
8+
branches: [main]
9+
workflow_dispatch:
510

6-
name: Quarto Publish
11+
# Serialize deploys so two pushes don't race on the gh-pages branch.
12+
concurrency:
13+
group: gh-pages
14+
cancel-in-progress: false
715

816
jobs:
9-
build-deploy:
10-
runs-on: ubuntu-latest
17+
publish:
1118
permissions:
19+
# Push the rendered site to gh-pages.
1220
contents: write
13-
steps:
14-
- name: Check out repository
15-
uses: actions/checkout@v4
21+
uses: Morrison-Lab/gha/.github/workflows/quarto-publish.yml@v2
22+
with:
23+
# macros-table.qmd and demo-include-in-header.qmd execute R chunks, and
24+
# this repo is neither an renv project nor an R package, so the packages
25+
# are named directly rather than resolved from a lockfile or DESCRIPTION.
26+
setup-r: true
27+
r-packages: |
28+
any::knitr
29+
any::rmarkdown
30+
any::DT
31+
# tinytex is not requested: _quarto.yml declares `format: html` only, so
32+
# nothing here renders through LaTeX. The macros themselves are typeset
33+
# client-side by MathJax, which needs no TeX installation on the runner.
1634

17-
- name: Set up Quarto
18-
uses: quarto-dev/quarto-actions/setup@v2
19-
env:
20-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21-
with:
22-
tinytex: true
23-
24-
- uses: r-lib/actions/setup-r@v2
25-
with:
26-
use-public-rspm: true
27-
28-
- uses: r-lib/actions/setup-r-dependencies@v2
29-
with:
30-
packages: |
31-
any::knitr
32-
any::rmarkdown
33-
any::DT
34-
35-
- name: Render
36-
uses: quarto-dev/quarto-actions/render@v2
37-
38-
- name: Deploy 🚀
39-
uses: JamesIves/github-pages-deploy-action@v4
40-
with:
41-
folder: _site/
42-
clean-exclude: pr-preview/
43-
force: false
35+
report-failure:
36+
needs: publish
37+
if: always() && needs.publish.result == 'failure'
38+
permissions:
39+
issues: write
40+
uses: Morrison-Lab/gha/.github/workflows/report-failure.yml@v2
41+
with:
42+
title: Quarto Publish workflow is failing
43+
body: The macros site did not deploy, so the published macro reference may be stale.

0 commit comments

Comments
 (0)