Skip to content
Open
Show file tree
Hide file tree
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
141 changes: 141 additions & 0 deletions .github/workflows/claude-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
name: Perform a code review when a pull request is created.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

jobs:
claude:
# Skip drafts, and skip fork PRs: on `pull_request` they don't get the
# ANTHROPIC_API_KEY secret, so the review would only fail.
if: >-
!github.event.pull_request.draft &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
structured_output: ${{ steps.run_claude.outputs.structured_output }}
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
# Explicitly check out the PR's merge commit.
ref: refs/pull/${{ github.event.pull_request.number }}/merge
persist-credentials: false

- name: Pre-fetch base and head refs for the PR
env:
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# Pass GitHub expressions through env and quote shell expansions.
git fetch --no-tags origin \
"$PR_BASE_REF" \
"+refs/pull/$PR_NUMBER/head"

# If you want Claude to build and run code, install any dependencies that
# need to be downloaded before the "Run Claude" step, and add the commands
# it may run to --allowedTools below (e.g. "Bash(npm test:*)"). Any tool
# not listed there is denied.

- name: Run Claude
id: run_claude
uses: anthropics/claude-code-action@86d88e619d8e6caf07b5c3944dd14f441c533718 # v1.0.243
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Use this job's read-only token instead of the Claude GitHub App,
# which needs `id-token: write` and would get write access.
github_token: ${{ github.token }}
prompt: |
This is PR #${{ github.event.pull_request.number }} for ${{ github.repository }}.

Review ONLY the changes introduced by the PR, so consider:
git log --oneline ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}
git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}

Suggest any improvements, potential bugs, or issues.
Be concise and specific in your feedback.

Return your review in two parts:
- `summary`: a short Markdown overview of the PR and your overall assessment.
- `comments`: one entry per specific issue, anchored to the line it concerns.
`path` is relative to the repository root. `line` is the line number in
the PR head version of the file (${{ github.event.pull_request.head.sha }}),
and must be a line shown in the diff above. The working tree is the merge
commit, so its line numbers can differ; check them with
`git show ${{ github.event.pull_request.head.sha }}:<path>`.
Put issues that don't belong to a single line in `summary` instead.

Pull request title and body:
----
${{ github.event.pull_request.title }}
${{ github.event.pull_request.body }}
claude_args: |
--allowedTools "Read,Grep,Glob,Bash(git log:*),Bash(git diff:*),Bash(git show:*)"
--json-schema '{
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "Overall review as Markdown, posted as the review body."
},
"comments": {
"type": "array",
"description": "Inline comments, each anchored to one line of the PR diff.",
"items": {
"type": "object",
"properties": {
"path": { "type": "string", "description": "File path relative to the repository root." },
"line": { "type": "integer", "minimum": 1, "description": "Line number in the PR head version of the file. Must be within the diff." },
"body": { "type": "string", "description": "The comment, as Markdown." }
},
"required": ["path", "line", "body"]
}
}
},
"required": ["summary", "comments"]
}'

post_feedback:
runs-on: ubuntu-latest
needs: claude
if: needs.claude.outputs.structured_output != ''
permissions:
issues: write
pull-requests: write
steps:
- name: Report Claude feedback
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
CLAUDE_STRUCTURED_OUTPUT: ${{ needs.claude.outputs.structured_output }}
with:
github-token: ${{ github.token }}
script: |
const { summary = '', comments = [] } = JSON.parse(process.env.CLAUDE_STRUCTURED_OUTPUT);
const pr = context.payload.pull_request;
if (!summary.trim() && comments.length === 0) {
core.info('Claude returned an empty review; nothing to post.');
return;
}

try {
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
commit_id: pr.head.sha,
event: 'COMMENT',
body: summary.trim() || 'See inline comments.',
comments: comments.map(({ path, line, body }) => ({ path, line, side: 'RIGHT', body })),
});
} catch (error) {
// GitHub rejects the whole review if any comment points outside the
// diff. Fall back to a single PR comment so no feedback is lost.
core.warning(`Could not post inline review (${error.message}); posting a plain comment instead.`);
const details = comments.map(({ path, line, body }) => `**\`${path}:${line}\`**\n\n${body}`);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: [summary.trim(), ...details].filter(Boolean).join('\n\n---\n\n'),
});
}
14 changes: 11 additions & 3 deletions crates/core/src/scenarios/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,23 @@ templates:
| `{ type: input, value: oracle }` | the pubkey given in that `input` property (`value_type: pubkey`, never written) |
| `{ type: pubkey, value: <address> }` | a fixed account |
| `{ type: pda, program_id, seeds }` | a derived account; seeds resolve against the override's values |
| `{ type: owned, owner: <program id or list>, account: <IDL account> }` | every local account an owner owns whose data starts with that account's discriminator in the keep-alive template's IDL, including accounts loaded later; never fetched |

`$slot`, `$slot±N` and `$unix_s`/`ms`/`us`/`ns` in `values` are filled from the slot and its Clock
time; other values are written as given.

Keep-alive starts in the slot the importing override plays and rewrites its fields every slot, after
that slot's overrides, until the network is reset. An override of one of those fields therefore
never takes effect. An account not yet in the surfnet is fetched from the remote first, unless it,
or its owner with its owned accounts, is marked offline. Keep-alive is kept in memory only: after a restart on a persistent database it stops
until an importing override plays again.
never takes effect. An override with `keepAlive: false` stops keep-alive for the account it writes
from that slot on, so the account ages as the clock moves on, for example to test a stale price. A
later override with `keepAlive: true`, the default, on that account starts it again, with every
import that rewrites it, including imports bound by other templates' overrides. An override with
`keepAlive: false` binds none of its template's imports, so on its own it starts no keep-alive, also
not for the other accounts those imports rewrite: add an override with `keepAlive: true` when they
must stay fresh. An override with no values writes nothing, so it neither starts nor stops
keep-alive. An account not yet in the surfnet is fetched from the remote first, unless it, or its
owner with its owned accounts, is marked offline. Keep-alive is kept in memory only: after a restart
on a persistent database it stops until an importing override plays again.

### Override Templates
Directly using the `surfnet_registerScenario` endpoint requires building out a map of account keys that are specific to the schema of the account that is being written to.
Expand Down
Loading
Loading