Compare commits

..

5 Commits

Author SHA1 Message Date
Ashwin Bhat
188d526721 refactor: change git hook from pre-push to pre-commit (#401)
- Renamed scripts/pre-push to scripts/pre-commit
- Updated install-hooks.sh to install pre-commit hook
- Hook now runs formatting, type checking, and tests before commit
2025-08-05 17:02:34 -07:00
Ashwin Bhat
a519840051 fix: remove git config user.name and user.email from allowed tools (#410)
These git config commands are no longer needed as allowed tools since
Claude should not be modifying git configuration settings. Updated
the corresponding test to reflect this intentional change.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-05 11:32:46 -07:00
yoshikouki
85287e957d fix: restore prompt file creation in agent mode (#405)
- Restore prompt file creation logic that was accidentally removed in PR #374
- Agent mode now creates the prompt file directly in prepare() method
- Uses override_prompt or direct_prompt if available, falls back to minimal prompt
- Fixes 'Prompt file does not exist' error for workflow_dispatch and schedule events
- Add TODO comment to refactor this to use createPrompt in the future

Fixes #403
2025-08-05 11:14:28 -07:00
GitHub Actions
c6a07895d7 chore: bump Claude Code version to 1.0.69 2025-08-05 16:50:23 +00:00
atsushi-ishibashi
0c5d54472f feat: Add HTML img tag support to GitHub image downloader (#402)
* feat: support html img tag

* rm files

* refactor
2025-08-04 19:37:50 -07:00
10 changed files with 292 additions and 14 deletions

View File

@@ -172,7 +172,7 @@ runs:
echo "Base-action dependencies installed"
cd -
# Install Claude Code globally
bun install -g @anthropic-ai/claude-code@1.0.68
bun install -g @anthropic-ai/claude-code@1.0.69
- name: Setup Network Restrictions
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''

View File

@@ -118,7 +118,7 @@ runs:
- name: Install Claude Code
shell: bash
run: bun install -g @anthropic-ai/claude-code@1.0.68
run: bun install -g @anthropic-ai/claude-code@1.0.69
- name: Run Claude Code Action
shell: bash

View File

@@ -6,8 +6,8 @@ echo "Installing git hooks..."
# Make sure hooks directory exists
mkdir -p .git/hooks
# Install pre-push hook
cp scripts/pre-push .git/hooks/pre-push
chmod +x .git/hooks/pre-push
# Install pre-commit hook
cp scripts/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
echo "Git hooks installed successfully!"

View File

@@ -60,8 +60,6 @@ export function buildAllowedToolsString(
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git rm:*)",
"Bash(git config user.name:*)",
"Bash(git config user.email:*)",
);
}

View File

@@ -33,7 +33,7 @@ export async function configureGitAuth(
if (user) {
const botName = user.login;
const botId = user.id;
console.log(`Setting git user as ${botName} (id: ${botId})...`);
console.log(`Setting git user as ${botName}...`);
await $`git config user.name "${botName}"`;
await $`git config user.email "${botId}+${botName}@${noreplyDomain}"`;
console.log(`✓ Set git user as ${botName}`);

View File

@@ -3,11 +3,17 @@ import path from "path";
import type { Octokits } from "../api/client";
import { GITHUB_SERVER_URL } from "../api/config";
const escapedUrl = GITHUB_SERVER_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const IMAGE_REGEX = new RegExp(
`!\\[[^\\]]*\\]\\((${GITHUB_SERVER_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\/user-attachments\\/assets\\/[^)]+)\\)`,
`!\\[[^\\]]*\\]\\((${escapedUrl}\\/user-attachments\\/assets\\/[^)]+)\\)`,
"g",
);
const HTML_IMG_REGEX = new RegExp(
`<img[^>]+src=["']([^"']*${escapedUrl}\\/user-attachments\\/assets\\/[^"']+)["'][^>]*>`,
"gi",
);
type IssueComment = {
type: "issue_comment";
id: string;
@@ -63,8 +69,16 @@ export async function downloadCommentImages(
}> = [];
for (const comment of comments) {
const imageMatches = [...comment.body.matchAll(IMAGE_REGEX)];
const urls = imageMatches.map((match) => match[1] as string);
// Extract URLs from Markdown format
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) {
commentsWithImages.push({ comment, urls });

View File

@@ -1,4 +1,5 @@
import * as core from "@actions/core";
import { mkdir, writeFile } from "fs/promises";
import type { Mode, ModeOptions, ModeResult } from "../types";
import { isAutomationContext } from "../../github/context";
import type { PreparedContext } from "../../create-prompt/types";
@@ -42,7 +43,23 @@ export const agentMode: Mode = {
async prepare({ context }: ModeOptions): Promise<ModeResult> {
// 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
const baseTools = [

View File

@@ -1041,8 +1041,6 @@ describe("buildAllowedToolsString", () => {
expect(result).toContain("Bash(git diff:*)");
expect(result).toContain("Bash(git log:*)");
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
expect(result).toContain("mcp__github_comment__update_claude_comment");

View File

@@ -662,4 +662,255 @@ describe("downloadCommentImages", () => {
);
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: ![test](${markdownUrl}) 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: ![test](${imageUrl}) 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",
);
});
});