Skip to content

Commit 9613b21

Browse files
committed
Merge branch 'main' of https://github.com/anthropics/claude-code-action into v1-dev
2 parents 30530c9 + 68b7ca3 commit 9613b21

14 files changed

Lines changed: 293 additions & 34 deletions

File tree

.github/workflows/issue-triage.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,5 @@ jobs:
104104
mcp_config: /tmp/mcp-config/mcp-servers.json
105105
timeout_minutes: "5"
106106
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
107+
env:
108+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

action.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ runs:
130130
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
131131
CLAUDE_ARGS: ${{ inputs.claude_args }}
132132
MCP_CONFIG: ${{ inputs.mcp_config }}
133+
ALL_INPUTS: ${{ toJson(inputs) }}
133134

134135
- name: Install Base Action Dependencies
135136
if: steps.prepare.outputs.contains_trigger == 'true'
@@ -141,7 +142,8 @@ runs:
141142
echo "Base-action dependencies installed"
142143
cd -
143144
# Install Claude Code globally
144-
bun install -g @anthropic-ai/claude-code@1.0.77
145+
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.84
146+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
145147
146148
- name: Setup Network Restrictions
147149
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
@@ -168,6 +170,7 @@ runs:
168170
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
169171
INPUT_CLAUDE_ARGS: ${{ steps.prepare.outputs.claude_args }}
170172
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
173+
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
171174

172175
# Model configuration
173176
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
@@ -241,7 +244,7 @@ runs:
241244
fi
242245
243246
- name: Revoke app token
244-
if: always() && inputs.github_token == ''
247+
if: always() && inputs.github_token == '' && steps.prepare.outputs.skipped_due_to_workflow_validation_mismatch != 'true'
245248
shell: bash
246249
run: |
247250
curl -L \

base-action/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ runs:
8585
8686
- name: Install Claude Code
8787
shell: bash
88-
run: bun install -g @anthropic-ai/claude-code@1.0.77
88+
run: curl -fsSL https://claude.ai/install.sh | bash -s 1.0.84
8989

9090
- name: Run Claude Code Action
9191
shell: bash

base-action/src/run-claude.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,16 @@ export function prepareRunConfig(
5656
}
5757
}
5858

59+
const customEnv: Record<string, string> = {};
60+
61+
if (process.env.INPUT_ACTION_INPUTS_PRESENT) {
62+
customEnv.GITHUB_ACTION_INPUTS = process.env.INPUT_ACTION_INPUTS_PRESENT;
63+
}
64+
5965
return {
6066
claudeArgs,
6167
promptPath,
62-
env: {},
68+
env: customEnv,
6369
};
6470
}
6571

@@ -88,9 +94,11 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
8894
console.log(`Prompt file size: ${promptSize} bytes`);
8995

9096
// Log custom environment variables if any
91-
if (Object.keys(config.env).length > 0) {
92-
const envKeys = Object.keys(config.env).join(", ");
93-
console.log(`Custom environment variables: ${envKeys}`);
97+
const customEnvKeys = Object.keys(config.env).filter(
98+
(key) => key !== "CLAUDE_ACTION_INPUTS_PRESENT",
99+
);
100+
if (customEnvKeys.length > 0) {
101+
console.log(`Custom environment variables: ${customEnvKeys.join(", ")}`);
94102
}
95103

96104
// Log custom arguments if any

examples/claude.yml

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Claude PR Assistant
1+
name: Claude Code
22

33
on:
44
issue_comment:
@@ -11,38 +11,53 @@ on:
1111
types: [submitted]
1212

1313
jobs:
14-
claude-code-action:
14+
claude:
1515
if: |
1616
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
1717
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
1818
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19-
(github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
19+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
2020
runs-on: ubuntu-latest
2121
permissions:
22-
contents: read
23-
pull-requests: read
24-
issues: read
22+
contents: write
23+
pull-requests: write
24+
issues: write
2525
id-token: write
26+
actions: read # Required for Claude to read CI results on PRs
2627
steps:
2728
- name: Checkout repository
2829
uses: actions/checkout@v4
2930
with:
3031
fetch-depth: 1
3132

32-
- name: Run Claude PR Action
33+
- name: Run Claude Code
34+
id: claude
3335
uses: anthropics/claude-code-action@beta
3436
with:
3537
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
36-
# Or use OAuth token instead:
37-
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38-
timeout_minutes: "60"
39-
# mode: tag # Default: responds to @claude mentions
40-
# Optional: Restrict network access to specific domains only
41-
# experimental_allowed_domains: |
42-
# .anthropic.com
43-
# .github.com
44-
# api.github.com
45-
# .githubusercontent.com
46-
# bun.sh
47-
# registry.npmjs.org
48-
# .blob.core.windows.net
38+
39+
# This is an optional setting that allows Claude to read CI results on PRs
40+
additional_permissions: |
41+
actions: read
42+
43+
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
44+
# model: "claude-opus-4-1-20250805"
45+
46+
# Optional: Customize the trigger phrase (default: @claude)
47+
# trigger_phrase: "/claude"
48+
49+
# Optional: Trigger when specific user is assigned to an issue
50+
# assignee_trigger: "claude-bot"
51+
52+
# Optional: Allow Claude to run specific commands
53+
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
54+
55+
# Optional: Add custom instructions for Claude to customize its behavior for your project
56+
# custom_instructions: |
57+
# Follow our coding standards
58+
# Ensure all new code has tests
59+
# Use TypeScript for new files
60+
61+
# Optional: Custom environment variables for Claude
62+
# claude_env: |
63+
# NODE_ENV: test

src/create-prompt/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,7 @@ export async function createPrompt(
750750
modeContext.claudeBranch,
751751
);
752752

753-
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
753+
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
754754
recursive: true,
755755
});
756756

@@ -769,7 +769,7 @@ export async function createPrompt(
769769

770770
// Write the prompt file
771771
await writeFile(
772-
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`,
772+
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
773773
promptContent,
774774
);
775775

src/entrypoints/collect-inputs.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as core from "@actions/core";
2+
3+
export function collectActionInputsPresence(): void {
4+
const inputDefaults: Record<string, string> = {
5+
trigger_phrase: "@claude",
6+
assignee_trigger: "",
7+
label_trigger: "claude",
8+
base_branch: "",
9+
branch_prefix: "claude/",
10+
allowed_bots: "",
11+
mode: "tag",
12+
model: "",
13+
anthropic_model: "",
14+
fallback_model: "",
15+
allowed_tools: "",
16+
disallowed_tools: "",
17+
custom_instructions: "",
18+
direct_prompt: "",
19+
override_prompt: "",
20+
mcp_config: "",
21+
additional_permissions: "",
22+
claude_env: "",
23+
settings: "",
24+
anthropic_api_key: "",
25+
claude_code_oauth_token: "",
26+
github_token: "",
27+
max_turns: "",
28+
use_sticky_comment: "false",
29+
use_commit_signing: "false",
30+
experimental_allowed_domains: "",
31+
};
32+
33+
const allInputsJson = process.env.ALL_INPUTS;
34+
if (!allInputsJson) {
35+
console.log("ALL_INPUTS environment variable not found");
36+
core.setOutput("action_inputs_present", JSON.stringify({}));
37+
return;
38+
}
39+
40+
let allInputs: Record<string, string>;
41+
try {
42+
allInputs = JSON.parse(allInputsJson);
43+
} catch (e) {
44+
console.error("Failed to parse ALL_INPUTS JSON:", e);
45+
core.setOutput("action_inputs_present", JSON.stringify({}));
46+
return;
47+
}
48+
49+
const presentInputs: Record<string, boolean> = {};
50+
51+
for (const [name, defaultValue] of Object.entries(inputDefaults)) {
52+
const actualValue = allInputs[name] || "";
53+
54+
const isSet = actualValue !== defaultValue;
55+
presentInputs[name] = isSet;
56+
}
57+
58+
core.setOutput("action_inputs_present", JSON.stringify(presentInputs));
59+
}

src/entrypoints/prepare.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ import { createOctokit } from "../github/api/client";
1212
import { parseGitHubContext, isEntityContext } from "../github/context";
1313
import { getMode } from "../modes/registry";
1414
import { prepare } from "../prepare";
15+
import { collectActionInputsPresence } from "./collect-inputs";
1516

1617
async function run() {
1718
try {
19+
collectActionInputsPresence();
20+
1821
// Parse GitHub context first to enable mode detection
1922
const context = parseGitHubContext();
2023

src/github/token.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,30 @@ async function exchangeForAppToken(oidcToken: string): Promise<string> {
3131
const responseJson = (await response.json()) as {
3232
error?: {
3333
message?: string;
34+
details?: {
35+
error_code?: string;
36+
};
3437
};
38+
type?: string;
39+
message?: string;
3540
};
41+
42+
// Check for specific workflow validation error codes that should skip the action
43+
const errorCode = responseJson.error?.details?.error_code;
44+
45+
if (errorCode === "workflow_not_found_on_default_branch") {
46+
const message =
47+
responseJson.message ??
48+
responseJson.error?.message ??
49+
"Workflow validation failed";
50+
core.warning(`Skipping action due to workflow validation: ${message}`);
51+
console.log(
52+
"Action skipped due to workflow validation error. This is expected when adding Claude Code workflows to new repositories or on PRs with workflow changes. If you're seeing this, your workflow will begin working once you merge your PR.",
53+
);
54+
core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
55+
process.exit(0);
56+
}
57+
3658
console.error(
3759
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson?.error?.message ?? "Unknown error"}`,
3860
);
@@ -77,8 +99,9 @@ export async function setupGitHubToken(): Promise<string> {
7799
core.setOutput("GITHUB_TOKEN", appToken);
78100
return appToken;
79101
} catch (error) {
102+
// Only set failed if we get here - workflow validation errors will exit(0) before this
80103
core.setFailed(
81-
`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.`,
104+
`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.`,
82105
);
83106
process.exit(1);
84107
}

src/github/utils/sanitizer.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,41 @@ export function sanitizeContent(content: string): string {
5858
content = stripMarkdownLinkTitles(content);
5959
content = stripHiddenAttributes(content);
6060
content = normalizeHtmlEntities(content);
61+
content = redactGitHubTokens(content);
62+
return content;
63+
}
64+
65+
export function redactGitHubTokens(content: string): string {
66+
// GitHub Personal Access Tokens (classic): ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
67+
content = content.replace(
68+
/\bghp_[A-Za-z0-9]{36}\b/g,
69+
"[REDACTED_GITHUB_TOKEN]",
70+
);
71+
72+
// GitHub OAuth tokens: gho_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
73+
content = content.replace(
74+
/\bgho_[A-Za-z0-9]{36}\b/g,
75+
"[REDACTED_GITHUB_TOKEN]",
76+
);
77+
78+
// GitHub installation tokens: ghs_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
79+
content = content.replace(
80+
/\bghs_[A-Za-z0-9]{36}\b/g,
81+
"[REDACTED_GITHUB_TOKEN]",
82+
);
83+
84+
// GitHub refresh tokens: ghr_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
85+
content = content.replace(
86+
/\bghr_[A-Za-z0-9]{36}\b/g,
87+
"[REDACTED_GITHUB_TOKEN]",
88+
);
89+
90+
// GitHub fine-grained personal access tokens: github_pat_XXXXXXXXXX (up to 255 chars)
91+
content = content.replace(
92+
/\bgithub_pat_[A-Za-z0-9_]{11,221}\b/g,
93+
"[REDACTED_GITHUB_TOKEN]",
94+
);
95+
6196
return content;
6297
}
6398

0 commit comments

Comments
 (0)