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
17 changes: 17 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,20 @@ Upload a screenshot of a bug and ask Claude to fix it:
```

Claude can see and analyze images, making it easy to fix visual bugs or UI issues.

### Analyze Images with GitHub Assets

For automation workflows that need to analyze images from GitHub issues or PRs, GitHub assets (images, attachments) are automatically downloaded when running in entity contexts (issue comments, pull requests, etc.):

```yaml
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Analyze any images attached to this issue/PR.

GitHub assets are automatically downloaded and available via CLAUDE_ASSET_FILES environment variable.
Use the Read tool to access and analyze each image file.
```

Images from GitHub issues and PRs are automatically downloaded and made available to Claude through the `CLAUDE_ASSET_FILES` environment variable when running in entity contexts.
24 changes: 24 additions & 0 deletions examples/agent-with-assets.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Agent Mode Asset Analysis
on:
issue_comment:
types: [created]

jobs:
analyze:
if: contains(github.event.comment.body, '@claude')
runs-on: ubuntu-latest
steps:
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Analyze the images from this GitHub issue/PR.

GitHub assets (images, attachments) are automatically downloaded and available
in the CLAUDE_ASSET_FILES environment variable.
You can use bash commands to process this environment variable and access the files.

For example, list the files with:
`echo $CLAUDE_ASSET_FILES | tr ',' '\n'`

Then use the Read tool to analyze each file.
32 changes: 32 additions & 0 deletions src/modes/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { PreparedContext } from "../../create-prompt/types";
import { prepareMcpConfig } from "../../mcp/install-mcp-server";
import { parseAllowedTools } from "./parse-tools";
import { configureGitAuth } from "../../github/operations/git-config";
import { fetchGitHubData } from "../../github/data/fetcher";
import { isEntityContext } from "../../github/context";

/**
* Agent mode implementation.
Expand Down Expand Up @@ -122,6 +124,36 @@ export const agentMode: Mode = {
// Append user's claude_args (which may have more --mcp-config flags)
claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim();

// Download GitHub assets automatically for entity contexts
if (isEntityContext(context)) {
console.log("Downloading GitHub assets for agent mode...");

try {
// Fetch GitHub data (reuse existing logic)
const githubData = await fetchGitHubData({
octokits: octokit,
repository: `${context.repository.owner}/${context.repository.repo}`,
prNumber: context.entityNumber.toString(),
isPR: context.isPR,
triggerUsername: context.actor,
});

// Set simple environment variable with comma-separated paths
const downloadedPaths = Array.from(githubData.imageUrlMap.values());
if (downloadedPaths.length > 0) {
const concatDownloadedPaths = downloadedPaths.join(",");
process.env.CLAUDE_ASSET_FILES = concatDownloadedPaths;
core.exportVariable("CLAUDE_ASSET_FILES", concatDownloadedPaths);
console.log(
`Exposed ${downloadedPaths.length} assets: ${concatDownloadedPaths}`,
);
}
} catch (error) {
console.error("Failed to download GitHub assets:", error);
// Continue execution - don't fail the entire action
}
}

core.setOutput("claude_args", claudeArgs);

return {
Expand Down
118 changes: 118 additions & 0 deletions test/modes/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { agentMode } from "../../src/modes/agent";
import type { GitHubContext } from "../../src/github/context";
import { createMockContext, createMockAutomationContext } from "../mockContext";
import * as core from "@actions/core";
import * as fetcher from "../../src/github/data/fetcher";

describe("Agent Mode", () => {
let mockContext: GitHubContext;
Expand Down Expand Up @@ -166,4 +167,121 @@ describe("Agent Mode", () => {
expect(callArgs[0]).toBe("claude_args");
expect(callArgs[1]).toContain("--mcp-config");
});

test("automatically downloads GitHub assets for entity context", async () => {
// Mock the fetchGitHubData function
const mockImageMap = new Map([
[
"https://github.com/user-attachments/assets/image1",
"/tmp/github-images/image-123.png",
],
[
"https://github.com/user-attachments/assets/image2",
"/tmp/github-images/image-456.jpg",
],
]);
const fetchSpy = spyOn(fetcher, "fetchGitHubData").mockResolvedValue({
contextData: {} as any,
comments: [],
changedFiles: [],
changedFilesWithSHA: [],
reviewData: null,
imageUrlMap: mockImageMap,
triggerDisplayName: null,
});

// Create an entity context (issue comment)
const entityContext = createMockContext({
eventName: "issue_comment",
inputs: { prompt: "Analyze images" },
});

const mockOctokit = {} as any;

await agentMode.prepare({
context: entityContext,
octokit: mockOctokit,
githubToken: "test-token",
});

// Verify fetchGitHubData was called
expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(fetchSpy).toHaveBeenCalledWith({
octokits: mockOctokit,
repository: `${entityContext.repository.owner}/${entityContext.repository.repo}`,
prNumber: entityContext.entityNumber.toString(),
isPR: entityContext.isPR,
triggerUsername: entityContext.actor,
});

// Verify CLAUDE_ASSET_FILES environment variable was set
expect(process.env.CLAUDE_ASSET_FILES).toBe(
"/tmp/github-images/image-123.png,/tmp/github-images/image-456.jpg",
);

// Verify core.exportVariable was called with correct arguments
expect(exportVariableSpy).toHaveBeenCalledWith(
"CLAUDE_ASSET_FILES",
"/tmp/github-images/image-123.png,/tmp/github-images/image-456.jpg",
);

// Clean up
delete process.env.CLAUDE_ASSET_FILES;
fetchSpy.mockRestore();
});

test("skips asset download for non-entity contexts", async () => {
const fetchSpy = spyOn(fetcher, "fetchGitHubData");

// Create an automation context (workflow_dispatch)
const automationContext = createMockAutomationContext({
eventName: "workflow_dispatch",
inputs: { prompt: "Analyze something" },
});

const mockOctokit = {} as any;

await agentMode.prepare({
context: automationContext,
octokit: mockOctokit,
githubToken: "test-token",
});

// Verify fetchGitHubData was NOT called for automation contexts
expect(fetchSpy).toHaveBeenCalledTimes(0);
expect(process.env.CLAUDE_ASSET_FILES).toBeUndefined();

fetchSpy.mockRestore();
});

test("handles asset download errors gracefully", async () => {
const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
const fetchSpy = spyOn(fetcher, "fetchGitHubData").mockRejectedValue(
new Error("Network error"),
);

const entityContext = createMockContext({
eventName: "issue_comment",
inputs: { prompt: "Analyze images" },
});

const mockOctokit = {} as any;

// This should not throw despite the error
await agentMode.prepare({
context: entityContext,
octokit: mockOctokit,
githubToken: "test-token",
});

// Verify error was logged but execution continued
expect(consoleSpy).toHaveBeenCalledWith(
"Failed to download GitHub assets:",
expect.any(Error),
);
expect(process.env.CLAUDE_ASSET_FILES).toBeUndefined();

fetchSpy.mockRestore();
consoleSpy.mockRestore();
});
});