Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0

name: Claude Code

on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request_review:
types: [submitted]
issues:
types: [opened]

jobs:
authorize:
# Use author_association as a cheap pre-filter so most unauthorized
# "@claude" mentions do not start a runner. The step below still checks
# the triggering actor's actual repository permission before the workflow
# grants write permissions or exposes the Anthropic API key.
if: |
(github.event_name == 'issue_comment' &&
contains(github.event.comment.body, '@claude') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) ||
(github.event_name == 'pull_request_review_comment' &&
contains(github.event.comment.body, '@claude') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) ||
(github.event_name == 'pull_request_review' &&
contains(github.event.review.body, '@claude') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) ||
(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))
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
has_write_access: ${{ steps.actor-permission.outputs.has_write_access }}
steps:
- name: Check actor repository permission
id: actor-permission
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TRIGGERING_ACTOR: ${{ github.actor }}
run: |
set -euo pipefail

permission="$(gh api "repos/${GH_REPO}/collaborators/${TRIGGERING_ACTOR}/permission" --jq '.permission' 2>/dev/null || true)"

case "${permission}" in
admin|maintain|write)
echo "has_write_access=true" >> "$GITHUB_OUTPUT"
;;
*)
echo "has_write_access=false" >> "$GITHUB_OUTPUT"
echo "Skipping Claude because ${TRIGGERING_ACTOR} does not have write access to ${GH_REPO}."
;;
esac

claude:
needs: authorize
if: needs.authorize.outputs.has_write_access == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this, I would much prefer if Claude could push all of its work to a fork and iterate on the fork, but that doesn't seem possible without extensive work. Would love thoughts here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just came to +1 this. I haven't even figured out how to get it to work on /my/ fork, and its habit of creating branches on the main fork makes the main fork much slower/harder to deal with.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern with direct iterative work on a public PR is reviewability: the intermediate agent churn can easily swamp the signal humans need. I’d rather keep AI iteration local and only surface milestones once a human decides the result is ready for shared review.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With Copilot I usually tell it to send a PR to my fork to avoid noise. Once I am happy, I send a PR upstream from the same branch. Example:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The benefit is that when upstream (CuPy) merges the PR, and I sync my fork with upstream, the fork PR is auto-closed. But it only works with the CuPy case because we do merge commits there instead of squash/rebase (yes, I am pointing fingers 😂 Squash/rebase is unfriendly to tooling and automation).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conda-forge bots always do work in their forks. Example: conda-forge/cccl-python-feedstock#35 was sent from https://github.com/regro-cf-autotick-bot/cccl-python-feedstock. I imagine for Claude & co to follow the same pattern, we'd probably need a service account...?

Copy link
Copy Markdown
Contributor

@mdboom mdboom May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern with direct iterative work on a public PR is reviewability: the intermediate agent churn can easily swamp the signal humans need. I’d rather keep AI iteration local and only surface milestones once a human decides the result is ready for shared review.

+1 to this. I have been thinking this as well lately. It shouldn't be part of the normal workflow to create a PR just to interact with an agent. It's not necessary -- I have plenty of ways to interact with an agent on my machine. It crowds out the main space where humans interact, which requires a communication/collaboration tool like this.

pull-requests: write
issues: write
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1

- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@62238ddb33772a079b0a6d8665a1ff3043583067 # v1.0.114
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}