mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 23:14:13 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
188d526721 | ||
|
|
a519840051 | ||
|
|
85287e957d | ||
|
|
c6a07895d7 | ||
|
|
0c5d54472f | ||
|
|
2845685880 |
@@ -172,7 +172,7 @@ 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.67
|
bun install -g @anthropic-ai/claude-code@1.0.69
|
||||||
|
|
||||||
- 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.67
|
run: bun install -g @anthropic-ai/claude-code@1.0.69
|
||||||
|
|
||||||
- name: Run Claude Code Action
|
- name: Run Claude Code Action
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ echo "Installing git hooks..."
|
|||||||
# Make sure hooks directory exists
|
# Make sure hooks directory exists
|
||||||
mkdir -p .git/hooks
|
mkdir -p .git/hooks
|
||||||
|
|
||||||
# Install pre-push hook
|
# Install pre-commit hook
|
||||||
cp scripts/pre-push .git/hooks/pre-push
|
cp scripts/pre-commit .git/hooks/pre-commit
|
||||||
chmod +x .git/hooks/pre-push
|
chmod +x .git/hooks/pre-commit
|
||||||
|
|
||||||
echo "Git hooks installed successfully!"
|
echo "Git hooks installed successfully!"
|
||||||
@@ -60,8 +60,6 @@ export function buildAllowedToolsString(
|
|||||||
"Bash(git diff:*)",
|
"Bash(git diff:*)",
|
||||||
"Bash(git log:*)",
|
"Bash(git log:*)",
|
||||||
"Bash(git rm:*)",
|
"Bash(git rm:*)",
|
||||||
"Bash(git config user.name:*)",
|
|
||||||
"Bash(git config user.email:*)",
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,17 @@ import path from "path";
|
|||||||
import type { Octokits } from "../api/client";
|
import type { Octokits } from "../api/client";
|
||||||
import { GITHUB_SERVER_URL } from "../api/config";
|
import { GITHUB_SERVER_URL } from "../api/config";
|
||||||
|
|
||||||
|
const escapedUrl = GITHUB_SERVER_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
const IMAGE_REGEX = new RegExp(
|
const IMAGE_REGEX = new RegExp(
|
||||||
`!\\[[^\\]]*\\]\\((${GITHUB_SERVER_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\/user-attachments\\/assets\\/[^)]+)\\)`,
|
`!\\[[^\\]]*\\]\\((${escapedUrl}\\/user-attachments\\/assets\\/[^)]+)\\)`,
|
||||||
"g",
|
"g",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const HTML_IMG_REGEX = new RegExp(
|
||||||
|
`<img[^>]+src=["']([^"']*${escapedUrl}\\/user-attachments\\/assets\\/[^"']+)["'][^>]*>`,
|
||||||
|
"gi",
|
||||||
|
);
|
||||||
|
|
||||||
type IssueComment = {
|
type IssueComment = {
|
||||||
type: "issue_comment";
|
type: "issue_comment";
|
||||||
id: string;
|
id: string;
|
||||||
@@ -63,8 +69,16 @@ export async function downloadCommentImages(
|
|||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
for (const comment of comments) {
|
for (const comment of comments) {
|
||||||
const imageMatches = [...comment.body.matchAll(IMAGE_REGEX)];
|
// Extract URLs from Markdown format
|
||||||
const urls = imageMatches.map((match) => match[1] as string);
|
const markdownMatches = [...comment.body.matchAll(IMAGE_REGEX)];
|
||||||
|
const markdownUrls = markdownMatches.map((match) => match[1] as string);
|
||||||
|
|
||||||
|
// Extract URLs from HTML format
|
||||||
|
const htmlMatches = [...comment.body.matchAll(HTML_IMG_REGEX)];
|
||||||
|
const htmlUrls = htmlMatches.map((match) => match[1] as string);
|
||||||
|
|
||||||
|
// Combine and deduplicate URLs
|
||||||
|
const urls = [...new Set([...markdownUrls, ...htmlUrls])];
|
||||||
|
|
||||||
if (urls.length > 0) {
|
if (urls.length > 0) {
|
||||||
commentsWithImages.push({ comment, urls });
|
commentsWithImages.push({ comment, urls });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import { mkdir, writeFile } from "fs/promises";
|
||||||
import type { Mode, ModeOptions, ModeResult } from "../types";
|
import type { Mode, ModeOptions, ModeResult } from "../types";
|
||||||
import { isAutomationContext } from "../../github/context";
|
import { isAutomationContext } from "../../github/context";
|
||||||
import type { PreparedContext } from "../../create-prompt/types";
|
import type { PreparedContext } from "../../create-prompt/types";
|
||||||
@@ -42,7 +43,23 @@ export const agentMode: Mode = {
|
|||||||
async prepare({ context }: ModeOptions): Promise<ModeResult> {
|
async prepare({ context }: ModeOptions): Promise<ModeResult> {
|
||||||
// Agent mode handles automation events (workflow_dispatch, schedule) only
|
// Agent mode handles automation events (workflow_dispatch, schedule) only
|
||||||
|
|
||||||
// Agent mode doesn't need to create prompt files here - handled by createPrompt
|
// TODO: handle by createPrompt (similar to tag and review modes)
|
||||||
|
// Create prompt directory
|
||||||
|
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
// Write the prompt file - the base action requires a prompt_file parameter,
|
||||||
|
// so we must create this file even though agent mode typically uses
|
||||||
|
// override_prompt or direct_prompt. If neither is provided, we write
|
||||||
|
// a minimal prompt with just the repository information.
|
||||||
|
const promptContent =
|
||||||
|
context.inputs.overridePrompt ||
|
||||||
|
context.inputs.directPrompt ||
|
||||||
|
`Repository: ${context.repository.owner}/${context.repository.repo}`;
|
||||||
|
await writeFile(
|
||||||
|
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`,
|
||||||
|
promptContent,
|
||||||
|
);
|
||||||
|
|
||||||
// Export tool environment variables for agent mode
|
// Export tool environment variables for agent mode
|
||||||
const baseTools = [
|
const baseTools = [
|
||||||
|
|||||||
@@ -1041,8 +1041,6 @@ describe("buildAllowedToolsString", () => {
|
|||||||
expect(result).toContain("Bash(git diff:*)");
|
expect(result).toContain("Bash(git diff:*)");
|
||||||
expect(result).toContain("Bash(git log:*)");
|
expect(result).toContain("Bash(git log:*)");
|
||||||
expect(result).toContain("Bash(git rm:*)");
|
expect(result).toContain("Bash(git rm:*)");
|
||||||
expect(result).toContain("Bash(git config user.name:*)");
|
|
||||||
expect(result).toContain("Bash(git config user.email:*)");
|
|
||||||
|
|
||||||
// Comment tool from minimal server should be included
|
// Comment tool from minimal server should be included
|
||||||
expect(result).toContain("mcp__github_comment__update_claude_comment");
|
expect(result).toContain("mcp__github_comment__update_claude_comment");
|
||||||
|
|||||||
@@ -662,4 +662,255 @@ describe("downloadCommentImages", () => {
|
|||||||
);
|
);
|
||||||
expect(result.get(imageUrl2)).toBeUndefined();
|
expect(result.get(imageUrl2)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should detect and download images from HTML img tags", async () => {
|
||||||
|
const mockOctokit = createMockOctokit();
|
||||||
|
const imageUrl =
|
||||||
|
"https://github.com/user-attachments/assets/html-image.png";
|
||||||
|
const signedUrl =
|
||||||
|
"https://private-user-images.githubusercontent.com/html.png?jwt=token";
|
||||||
|
|
||||||
|
// Mock octokit response
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
body_html: `<img src="${signedUrl}">`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock fetch for image download
|
||||||
|
const mockArrayBuffer = new ArrayBuffer(8);
|
||||||
|
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
arrayBuffer: async () => mockArrayBuffer,
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const comments: CommentWithImages[] = [
|
||||||
|
{
|
||||||
|
type: "issue_comment",
|
||||||
|
id: "777",
|
||||||
|
body: `Here's an HTML image: <img src="${imageUrl}" alt="test">`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await downloadCommentImages(
|
||||||
|
mockOctokit,
|
||||||
|
"owner",
|
||||||
|
"repo",
|
||||||
|
comments,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockOctokit.rest.issues.getComment).toHaveBeenCalledWith({
|
||||||
|
owner: "owner",
|
||||||
|
repo: "repo",
|
||||||
|
comment_id: 777,
|
||||||
|
mediaType: { format: "full+json" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fetchSpy).toHaveBeenCalledWith(signedUrl);
|
||||||
|
expect(fsWriteFileSpy).toHaveBeenCalledWith(
|
||||||
|
"/tmp/github-images/image-1704067200000-0.png",
|
||||||
|
Buffer.from(mockArrayBuffer),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.size).toBe(1);
|
||||||
|
expect(result.get(imageUrl)).toBe(
|
||||||
|
"/tmp/github-images/image-1704067200000-0.png",
|
||||||
|
);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
"Found 1 image(s) in issue_comment 777",
|
||||||
|
);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(`Downloading ${imageUrl}...`);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
"✓ Saved: /tmp/github-images/image-1704067200000-0.png",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle HTML img tags with different quote styles", async () => {
|
||||||
|
const mockOctokit = createMockOctokit();
|
||||||
|
const imageUrl1 =
|
||||||
|
"https://github.com/user-attachments/assets/single-quote.jpg";
|
||||||
|
const imageUrl2 =
|
||||||
|
"https://github.com/user-attachments/assets/double-quote.png";
|
||||||
|
const signedUrl1 =
|
||||||
|
"https://private-user-images.githubusercontent.com/single.jpg?jwt=token1";
|
||||||
|
const signedUrl2 =
|
||||||
|
"https://private-user-images.githubusercontent.com/double.png?jwt=token2";
|
||||||
|
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
body_html: `<img src="${signedUrl1}"><img src="${signedUrl2}">`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
arrayBuffer: async () => new ArrayBuffer(8),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const comments: CommentWithImages[] = [
|
||||||
|
{
|
||||||
|
type: "issue_comment",
|
||||||
|
id: "888",
|
||||||
|
body: `Single quote: <img src='${imageUrl1}' alt="test"> and double quote: <img src="${imageUrl2}" alt="test">`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await downloadCommentImages(
|
||||||
|
mockOctokit,
|
||||||
|
"owner",
|
||||||
|
"repo",
|
||||||
|
comments,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(result.size).toBe(2);
|
||||||
|
expect(result.get(imageUrl1)).toBe(
|
||||||
|
"/tmp/github-images/image-1704067200000-0.jpg",
|
||||||
|
);
|
||||||
|
expect(result.get(imageUrl2)).toBe(
|
||||||
|
"/tmp/github-images/image-1704067200000-1.png",
|
||||||
|
);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
"Found 2 image(s) in issue_comment 888",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle mixed Markdown and HTML images", async () => {
|
||||||
|
const mockOctokit = createMockOctokit();
|
||||||
|
const markdownUrl =
|
||||||
|
"https://github.com/user-attachments/assets/markdown.png";
|
||||||
|
const htmlUrl = "https://github.com/user-attachments/assets/html.jpg";
|
||||||
|
const signedUrl1 =
|
||||||
|
"https://private-user-images.githubusercontent.com/md.png?jwt=token1";
|
||||||
|
const signedUrl2 =
|
||||||
|
"https://private-user-images.githubusercontent.com/html.jpg?jwt=token2";
|
||||||
|
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
body_html: `<img src="${signedUrl1}"><img src="${signedUrl2}">`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
arrayBuffer: async () => new ArrayBuffer(8),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const comments: CommentWithImages[] = [
|
||||||
|
{
|
||||||
|
type: "issue_comment",
|
||||||
|
id: "999",
|
||||||
|
body: `Markdown:  and HTML: <img src="${htmlUrl}" alt="test">`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await downloadCommentImages(
|
||||||
|
mockOctokit,
|
||||||
|
"owner",
|
||||||
|
"repo",
|
||||||
|
comments,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(result.size).toBe(2);
|
||||||
|
expect(result.get(markdownUrl)).toBe(
|
||||||
|
"/tmp/github-images/image-1704067200000-0.png",
|
||||||
|
);
|
||||||
|
expect(result.get(htmlUrl)).toBe(
|
||||||
|
"/tmp/github-images/image-1704067200000-1.jpg",
|
||||||
|
);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
"Found 2 image(s) in issue_comment 999",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should deduplicate identical URLs from Markdown and HTML", async () => {
|
||||||
|
const mockOctokit = createMockOctokit();
|
||||||
|
const imageUrl = "https://github.com/user-attachments/assets/duplicate.png";
|
||||||
|
const signedUrl =
|
||||||
|
"https://private-user-images.githubusercontent.com/dup.png?jwt=token";
|
||||||
|
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
body_html: `<img src="${signedUrl}">`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
arrayBuffer: async () => new ArrayBuffer(8),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const comments: CommentWithImages[] = [
|
||||||
|
{
|
||||||
|
type: "issue_comment",
|
||||||
|
id: "1000",
|
||||||
|
body: `Same image twice:  and <img src="${imageUrl}" alt="test">`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await downloadCommentImages(
|
||||||
|
mockOctokit,
|
||||||
|
"owner",
|
||||||
|
"repo",
|
||||||
|
comments,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fetchSpy).toHaveBeenCalledTimes(1); // Only downloaded once
|
||||||
|
expect(result.size).toBe(1);
|
||||||
|
expect(result.get(imageUrl)).toBe(
|
||||||
|
"/tmp/github-images/image-1704067200000-0.png",
|
||||||
|
);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
"Found 1 image(s) in issue_comment 1000",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle HTML img tags with additional attributes", async () => {
|
||||||
|
const mockOctokit = createMockOctokit();
|
||||||
|
const imageUrl =
|
||||||
|
"https://github.com/user-attachments/assets/complex-tag.webp";
|
||||||
|
const signedUrl =
|
||||||
|
"https://private-user-images.githubusercontent.com/complex.webp?jwt=token";
|
||||||
|
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
body_html: `<img src="${signedUrl}">`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
arrayBuffer: async () => new ArrayBuffer(8),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const comments: CommentWithImages[] = [
|
||||||
|
{
|
||||||
|
type: "issue_comment",
|
||||||
|
id: "1001",
|
||||||
|
body: `Complex tag: <img class="image" src="${imageUrl}" alt="test image" width="100" height="200">`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await downloadCommentImages(
|
||||||
|
mockOctokit,
|
||||||
|
"owner",
|
||||||
|
"repo",
|
||||||
|
comments,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result.size).toBe(1);
|
||||||
|
expect(result.get(imageUrl)).toBe(
|
||||||
|
"/tmp/github-images/image-1704067200000-0.webp",
|
||||||
|
);
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||||
|
"Found 1 image(s) in issue_comment 1001",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user