mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 15:04:13 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02e9ed3181 | ||
|
|
78b07473f5 | ||
|
|
f562ed53e2 | ||
|
|
a1507aefdc | ||
|
|
ae66eb6a64 | ||
|
|
432c7cc889 | ||
|
|
0b138d9d49 | ||
|
|
c34e066a3b | ||
|
|
449c6791bd | ||
|
|
2b67ac084b | ||
|
|
76de8a48fc |
@@ -177,7 +177,8 @@ runs:
|
|||||||
echo "Base-action dependencies installed"
|
echo "Base-action dependencies installed"
|
||||||
cd -
|
cd -
|
||||||
# Install Claude Code globally
|
# Install Claude Code globally
|
||||||
bun install -g @anthropic-ai/claude-code@1.0.77
|
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.83
|
||||||
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Setup Network Restrictions
|
- name: Setup Network Restrictions
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
|
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ runs:
|
|||||||
|
|
||||||
- name: Install Claude Code
|
- name: Install Claude Code
|
||||||
shell: bash
|
shell: bash
|
||||||
run: bun install -g @anthropic-ai/claude-code@1.0.77
|
run: curl -fsSL https://claude.ai/install.sh | bash -s 1.0.83
|
||||||
|
|
||||||
- name: Run Claude Code Action
|
- name: Run Claude Code Action
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Claude PR Assistant
|
name: Claude Code
|
||||||
|
|
||||||
on:
|
on:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
@@ -11,38 +11,53 @@ on:
|
|||||||
types: [submitted]
|
types: [submitted]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
claude-code-action:
|
claude:
|
||||||
if: |
|
if: |
|
||||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||||
(github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
|
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: write
|
||||||
pull-requests: read
|
pull-requests: write
|
||||||
issues: read
|
issues: write
|
||||||
id-token: write
|
id-token: write
|
||||||
|
actions: read # Required for Claude to read CI results on PRs
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Run Claude PR Action
|
- name: Run Claude Code
|
||||||
|
id: claude
|
||||||
uses: anthropics/claude-code-action@beta
|
uses: anthropics/claude-code-action@beta
|
||||||
with:
|
with:
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
# Or use OAuth token instead:
|
|
||||||
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
# This is an optional setting that allows Claude to read CI results on PRs
|
||||||
timeout_minutes: "60"
|
additional_permissions: |
|
||||||
# mode: tag # Default: responds to @claude mentions
|
actions: read
|
||||||
# Optional: Restrict network access to specific domains only
|
|
||||||
# experimental_allowed_domains: |
|
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
|
||||||
# .anthropic.com
|
# model: "claude-opus-4-1-20250805"
|
||||||
# .github.com
|
|
||||||
# api.github.com
|
# Optional: Customize the trigger phrase (default: @claude)
|
||||||
# .githubusercontent.com
|
# trigger_phrase: "/claude"
|
||||||
# bun.sh
|
|
||||||
# registry.npmjs.org
|
# Optional: Trigger when specific user is assigned to an issue
|
||||||
# .blob.core.windows.net
|
# assignee_trigger: "claude-bot"
|
||||||
|
|
||||||
|
# Optional: Allow Claude to run specific commands
|
||||||
|
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
|
||||||
|
|
||||||
|
# Optional: Add custom instructions for Claude to customize its behavior for your project
|
||||||
|
# custom_instructions: |
|
||||||
|
# Follow our coding standards
|
||||||
|
# Ensure all new code has tests
|
||||||
|
# Use TypeScript for new files
|
||||||
|
|
||||||
|
# Optional: Custom environment variables for Claude
|
||||||
|
# claude_env: |
|
||||||
|
# NODE_ENV: test
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export async function setupGitHubToken(): Promise<string> {
|
|||||||
return appToken;
|
return appToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.setFailed(
|
core.setFailed(
|
||||||
`Failed to setup GitHub token: ${error}.\n\nIf you instead wish to use this action with a custom GitHub token or custom GitHub app, provide a \`github_token\` in the \`uses\` section of the app in your workflow yml file.`,
|
`Failed to setup GitHub token: ${error}\n\nIf you instead wish to use this action with a custom GitHub token or custom GitHub app, provide a \`github_token\` in the \`uses\` section of the app in your workflow yml file.`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,41 @@ export function sanitizeContent(content: string): string {
|
|||||||
content = stripMarkdownLinkTitles(content);
|
content = stripMarkdownLinkTitles(content);
|
||||||
content = stripHiddenAttributes(content);
|
content = stripHiddenAttributes(content);
|
||||||
content = normalizeHtmlEntities(content);
|
content = normalizeHtmlEntities(content);
|
||||||
|
content = redactGitHubTokens(content);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redactGitHubTokens(content: string): string {
|
||||||
|
// GitHub Personal Access Tokens (classic): ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||||
|
content = content.replace(
|
||||||
|
/\bghp_[A-Za-z0-9]{36}\b/g,
|
||||||
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
|
||||||
|
// GitHub OAuth tokens: gho_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||||
|
content = content.replace(
|
||||||
|
/\bgho_[A-Za-z0-9]{36}\b/g,
|
||||||
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
|
||||||
|
// GitHub installation tokens: ghs_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||||
|
content = content.replace(
|
||||||
|
/\bghs_[A-Za-z0-9]{36}\b/g,
|
||||||
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
|
||||||
|
// GitHub refresh tokens: ghr_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||||
|
content = content.replace(
|
||||||
|
/\bghr_[A-Za-z0-9]{36}\b/g,
|
||||||
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
|
||||||
|
// GitHub fine-grained personal access tokens: github_pat_XXXXXXXXXX (up to 255 chars)
|
||||||
|
content = content.replace(
|
||||||
|
/\bgithub_pat_[A-Za-z0-9_]{11,221}\b/g,
|
||||||
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|||||||
import { GITHUB_API_URL } from "../github/api/config";
|
import { GITHUB_API_URL } from "../github/api/config";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
|
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
|
||||||
|
import { sanitizeContent } from "../github/utils/sanitizer";
|
||||||
|
|
||||||
// Get repository information from environment variables
|
// Get repository information from environment variables
|
||||||
const REPO_OWNER = process.env.REPO_OWNER;
|
const REPO_OWNER = process.env.REPO_OWNER;
|
||||||
@@ -54,11 +55,13 @@ server.tool(
|
|||||||
const isPullRequestReviewComment =
|
const isPullRequestReviewComment =
|
||||||
eventName === "pull_request_review_comment";
|
eventName === "pull_request_review_comment";
|
||||||
|
|
||||||
|
const sanitizedBody = sanitizeContent(body);
|
||||||
|
|
||||||
const result = await updateClaudeComment(octokit, {
|
const result = await updateClaudeComment(octokit, {
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
commentId,
|
commentId,
|
||||||
body,
|
body: sanitizedBody,
|
||||||
isPullRequestReviewComment,
|
isPullRequestReviewComment,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createOctokit } from "../github/api/client";
|
import { createOctokit } from "../github/api/client";
|
||||||
|
import { sanitizeContent } from "../github/utils/sanitizer";
|
||||||
|
|
||||||
// Get repository and PR information from environment variables
|
// Get repository and PR information from environment variables
|
||||||
const REPO_OWNER = process.env.REPO_OWNER;
|
const REPO_OWNER = process.env.REPO_OWNER;
|
||||||
@@ -81,6 +82,9 @@ server.tool(
|
|||||||
|
|
||||||
const octokit = createOctokit(githubToken).rest;
|
const octokit = createOctokit(githubToken).rest;
|
||||||
|
|
||||||
|
// Sanitize the comment body to remove any potential GitHub tokens
|
||||||
|
const sanitizedBody = sanitizeContent(body);
|
||||||
|
|
||||||
// Validate that either line or both startLine and line are provided
|
// Validate that either line or both startLine and line are provided
|
||||||
if (!line && !startLine) {
|
if (!line && !startLine) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -104,7 +108,7 @@ server.tool(
|
|||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
pull_number,
|
pull_number,
|
||||||
body,
|
body: sanitizedBody,
|
||||||
path,
|
path,
|
||||||
side: side || "RIGHT",
|
side: side || "RIGHT",
|
||||||
commit_id: commit_id || pr.data.head.sha,
|
commit_id: commit_id || pr.data.head.sha,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
normalizeHtmlEntities,
|
normalizeHtmlEntities,
|
||||||
sanitizeContent,
|
sanitizeContent,
|
||||||
stripHtmlComments,
|
stripHtmlComments,
|
||||||
|
redactGitHubTokens,
|
||||||
} from "../src/github/utils/sanitizer";
|
} from "../src/github/utils/sanitizer";
|
||||||
|
|
||||||
describe("stripInvisibleCharacters", () => {
|
describe("stripInvisibleCharacters", () => {
|
||||||
@@ -242,6 +243,109 @@ describe("sanitizeContent", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("redactGitHubTokens", () => {
|
||||||
|
it("should redact personal access tokens (ghp_)", () => {
|
||||||
|
const token = "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";
|
||||||
|
expect(redactGitHubTokens(`Token: ${token}`)).toBe(
|
||||||
|
"Token: [REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
expect(redactGitHubTokens(`Here's a token: ${token} in text`)).toBe(
|
||||||
|
"Here's a token: [REDACTED_GITHUB_TOKEN] in text",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact OAuth tokens (gho_)", () => {
|
||||||
|
const token = "gho_16C7e42F292c6912E7710c838347Ae178B4a";
|
||||||
|
expect(redactGitHubTokens(`OAuth: ${token}`)).toBe(
|
||||||
|
"OAuth: [REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact installation tokens (ghs_)", () => {
|
||||||
|
const token = "ghs_xz7yzju2SZjGPa0dUNMAx0SH4xDOCS31LXQW";
|
||||||
|
expect(redactGitHubTokens(`Install token: ${token}`)).toBe(
|
||||||
|
"Install token: [REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact refresh tokens (ghr_)", () => {
|
||||||
|
const token = "ghr_1B4a2e77838347a253e56d7b5253e7d11667";
|
||||||
|
expect(redactGitHubTokens(`Refresh: ${token}`)).toBe(
|
||||||
|
"Refresh: [REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact fine-grained tokens (github_pat_)", () => {
|
||||||
|
const token =
|
||||||
|
"github_pat_11ABCDEFG0example5of9_2nVwvsylpmOLboQwTPTLewDcE621dQ0AAaBBCCDDEEFFHH";
|
||||||
|
expect(redactGitHubTokens(`Fine-grained: ${token}`)).toBe(
|
||||||
|
"Fine-grained: [REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle tokens in code blocks", () => {
|
||||||
|
const content = `\`\`\`bash
|
||||||
|
export GITHUB_TOKEN=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
|
||||||
|
\`\`\``;
|
||||||
|
const expected = `\`\`\`bash
|
||||||
|
export GITHUB_TOKEN=[REDACTED_GITHUB_TOKEN]
|
||||||
|
\`\`\``;
|
||||||
|
expect(redactGitHubTokens(content)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple tokens in one text", () => {
|
||||||
|
const content =
|
||||||
|
"Token 1: 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 and token 2: gho_16C7e42F292c6912E7710c838347Ae178B4a";
|
||||||
|
expect(redactGitHubTokens(content)).toBe(
|
||||||
|
"Token 1: [REDACTED_GITHUB_TOKEN] and token 2: [REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle tokens in URLs", () => {
|
||||||
|
const content =
|
||||||
|
"https://api.github.com/user?access_token=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";
|
||||||
|
expect(redactGitHubTokens(content)).toBe(
|
||||||
|
"https://api.github.com/user?access_token=[REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not redact partial matches or invalid tokens", () => {
|
||||||
|
const content =
|
||||||
|
"This is not a token: ghp_short or gho_toolong1234567890123456789012345678901234567890";
|
||||||
|
expect(redactGitHubTokens(content)).toBe(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve normal text", () => {
|
||||||
|
const content = "Normal text with no tokens";
|
||||||
|
expect(redactGitHubTokens(content)).toBe(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle edge cases", () => {
|
||||||
|
expect(redactGitHubTokens("")).toBe("");
|
||||||
|
expect(redactGitHubTokens("ghp_")).toBe("ghp_");
|
||||||
|
expect(redactGitHubTokens("github_pat_short")).toBe("github_pat_short");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sanitizeContent with token redaction", () => {
|
||||||
|
it("should redact tokens as part of full sanitization", () => {
|
||||||
|
const content = `
|
||||||
|
<!-- Hidden comment with token: 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 -->
|
||||||
|
Here's some text with a token: gho_16C7e42F292c6912E7710c838347Ae178B4a
|
||||||
|
And invisible chars: test\u200Btoken
|
||||||
|
`;
|
||||||
|
|
||||||
|
const sanitized = sanitizeContent(content);
|
||||||
|
|
||||||
|
expect(sanitized).not.toContain("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");
|
||||||
|
expect(sanitized).not.toContain("gho_16C7e42F292c6912E7710c838347Ae178B4a");
|
||||||
|
expect(sanitized).not.toContain("<!-- Hidden comment");
|
||||||
|
expect(sanitized).not.toContain("\u200B");
|
||||||
|
expect(sanitized).toContain("[REDACTED_GITHUB_TOKEN]");
|
||||||
|
expect(sanitized).toContain("Here's some text with a token:");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("stripHtmlComments (legacy)", () => {
|
describe("stripHtmlComments (legacy)", () => {
|
||||||
it("should remove HTML comments", () => {
|
it("should remove HTML comments", () => {
|
||||||
expect(stripHtmlComments("Hello <!-- example -->World")).toBe(
|
expect(stripHtmlComments("Hello <!-- example -->World")).toBe(
|
||||||
|
|||||||
Reference in New Issue
Block a user