mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-26 00:34:13 +08:00
Compare commits
3 Commits
v1.0.33
...
ashwin/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef768ce12d | ||
|
|
774d5204a1 | ||
|
|
f64219702d |
@@ -213,7 +213,7 @@ runs:
|
|||||||
|
|
||||||
# Install Claude Code if no custom executable is provided
|
# Install Claude Code if no custom executable is provided
|
||||||
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
||||||
CLAUDE_CODE_VERSION="2.1.17"
|
CLAUDE_CODE_VERSION="2.1.19"
|
||||||
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
echo "Installation attempt $attempt..."
|
echo "Installation attempt $attempt..."
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ runs:
|
|||||||
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
||||||
run: |
|
run: |
|
||||||
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
||||||
CLAUDE_CODE_VERSION="2.1.17"
|
CLAUDE_CODE_VERSION="2.1.19"
|
||||||
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
echo "Installation attempt $attempt..."
|
echo "Installation attempt $attempt..."
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"name": "@anthropic-ai/claude-code-base-action",
|
"name": "@anthropic-ai/claude-code-base-action",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.17",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.19",
|
||||||
"shell-quote": "^1.8.3",
|
"shell-quote": "^1.8.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||||
|
|
||||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.17", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-cWEZ3fhPG6beVlZkXPAGYwqoR5zbELPracg+eKQ9UUqlf9m5UmUaaasGSXdVVxgDkjZfl8yoPsHnicuL2GIB1A=="],
|
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.19", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-DjaX4t3Swjt5PcsZt6krcp5TfBTRxVuUZhkY6L8WWF8kZBJFuuEd5akNg486XRskTXGuwLmitxp0wHB1hJ9muw=="],
|
||||||
|
|
||||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.17",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.19",
|
||||||
"shell-quote": "^1.8.3"
|
"shell-quote": "^1.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
4
bun.lock
4
bun.lock
@@ -7,7 +7,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@actions/github": "^6.0.1",
|
"@actions/github": "^6.0.1",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.17",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.19",
|
||||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||||
"@octokit/graphql": "^8.2.2",
|
"@octokit/graphql": "^8.2.2",
|
||||||
"@octokit/rest": "^21.1.1",
|
"@octokit/rest": "^21.1.1",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||||
|
|
||||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.17", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-cWEZ3fhPG6beVlZkXPAGYwqoR5zbELPracg+eKQ9UUqlf9m5UmUaaasGSXdVVxgDkjZfl8yoPsHnicuL2GIB1A=="],
|
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.19", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-DjaX4t3Swjt5PcsZt6krcp5TfBTRxVuUZhkY6L8WWF8kZBJFuuEd5akNg486XRskTXGuwLmitxp0wHB1hJ9muw=="],
|
||||||
|
|
||||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||||
|
|
||||||
|
|||||||
@@ -172,9 +172,14 @@ jobs:
|
|||||||
|
|
||||||
**Important Notes**:
|
**Important Notes**:
|
||||||
|
|
||||||
- The GitHub token must have the `actions: read` permission in your workflow
|
- The GitHub token must have the corresponding permission in your workflow
|
||||||
- If the permission is missing, Claude will warn you and suggest adding it
|
- If the permission is missing, Claude will warn you and suggest adding it
|
||||||
- Currently, only `actions: read` is supported, but the format allows for future extensions
|
- The following additional permissions can be requested beyond the defaults:
|
||||||
|
- `actions: read`
|
||||||
|
- `checks: read`
|
||||||
|
- `discussions: read` or `discussions: write`
|
||||||
|
- `workflows: read` or `workflows: write`
|
||||||
|
- Standard permissions (`contents: write`, `pull_requests: write`, `issues: write`) are always included and do not need to be specified
|
||||||
|
|
||||||
## Custom Environment Variables
|
## Custom Environment Variables
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@actions/github": "^6.0.1",
|
"@actions/github": "^6.0.1",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.17",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.19",
|
||||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||||
"@octokit/graphql": "^8.2.2",
|
"@octokit/graphql": "^8.2.2",
|
||||||
"@octokit/rest": "^21.1.1",
|
"@octokit/rest": "^21.1.1",
|
||||||
|
|||||||
@@ -16,15 +16,60 @@ async function getOidcToken(): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exchangeForAppToken(oidcToken: string): Promise<string> {
|
const DEFAULT_PERMISSIONS: Record<string, string> = {
|
||||||
|
contents: "write",
|
||||||
|
pull_requests: "write",
|
||||||
|
issues: "write",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseAdditionalPermissions():
|
||||||
|
| Record<string, string>
|
||||||
|
| undefined {
|
||||||
|
const raw = process.env.ADDITIONAL_PERMISSIONS;
|
||||||
|
if (!raw || !raw.trim()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const additional: Record<string, string> = {};
|
||||||
|
for (const line of raw.split("\n")) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
const colonIndex = trimmed.indexOf(":");
|
||||||
|
if (colonIndex === -1) continue;
|
||||||
|
const key = trimmed.slice(0, colonIndex).trim();
|
||||||
|
const value = trimmed.slice(colonIndex + 1).trim();
|
||||||
|
if (key && value) {
|
||||||
|
additional[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(additional).length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...DEFAULT_PERMISSIONS, ...additional };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exchangeForAppToken(
|
||||||
|
oidcToken: string,
|
||||||
|
permissions?: Record<string, string>,
|
||||||
|
): Promise<string> {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Authorization: `Bearer ${oidcToken}`,
|
||||||
|
};
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (permissions) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
fetchOptions.body = JSON.stringify({ permissions });
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"https://api.anthropic.com/api/github/github-app-token-exchange",
|
"https://api.anthropic.com/api/github/github-app-token-exchange",
|
||||||
{
|
fetchOptions,
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${oidcToken}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -89,9 +134,11 @@ export async function setupGitHubToken(): Promise<string> {
|
|||||||
const oidcToken = await retryWithBackoff(() => getOidcToken());
|
const oidcToken = await retryWithBackoff(() => getOidcToken());
|
||||||
console.log("OIDC token successfully obtained");
|
console.log("OIDC token successfully obtained");
|
||||||
|
|
||||||
|
const permissions = parseAdditionalPermissions();
|
||||||
|
|
||||||
console.log("Exchanging OIDC token for app token...");
|
console.log("Exchanging OIDC token for app token...");
|
||||||
const appToken = await retryWithBackoff(() =>
|
const appToken = await retryWithBackoff(() =>
|
||||||
exchangeForAppToken(oidcToken),
|
exchangeForAppToken(oidcToken, permissions),
|
||||||
);
|
);
|
||||||
console.log("App token successfully obtained");
|
console.log("App token successfully obtained");
|
||||||
|
|
||||||
|
|||||||
97
test/parse-permissions.test.ts
Normal file
97
test/parse-permissions.test.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
||||||
|
import { parseAdditionalPermissions } from "../src/github/token";
|
||||||
|
|
||||||
|
describe("parseAdditionalPermissions", () => {
|
||||||
|
let originalEnv: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalEnv = process.env.ADDITIONAL_PERMISSIONS;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalEnv === undefined) {
|
||||||
|
delete process.env.ADDITIONAL_PERMISSIONS;
|
||||||
|
} else {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS = originalEnv;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined when env var is not set", () => {
|
||||||
|
delete process.env.ADDITIONAL_PERMISSIONS;
|
||||||
|
expect(parseAdditionalPermissions()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined when env var is empty string", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS = "";
|
||||||
|
expect(parseAdditionalPermissions()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined when env var is only whitespace", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS = " \n \n ";
|
||||||
|
expect(parseAdditionalPermissions()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses single permission and merges with defaults", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS = "actions: read";
|
||||||
|
expect(parseAdditionalPermissions()).toEqual({
|
||||||
|
contents: "write",
|
||||||
|
pull_requests: "write",
|
||||||
|
issues: "write",
|
||||||
|
actions: "read",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses multiple permissions", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS = "actions: read\nworkflows: write";
|
||||||
|
expect(parseAdditionalPermissions()).toEqual({
|
||||||
|
contents: "write",
|
||||||
|
pull_requests: "write",
|
||||||
|
issues: "write",
|
||||||
|
actions: "read",
|
||||||
|
workflows: "write",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("additional permissions can override defaults", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS = "contents: read";
|
||||||
|
expect(parseAdditionalPermissions()).toEqual({
|
||||||
|
contents: "read",
|
||||||
|
pull_requests: "write",
|
||||||
|
issues: "write",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles extra whitespace around keys and values", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS = " actions : read ";
|
||||||
|
expect(parseAdditionalPermissions()).toEqual({
|
||||||
|
contents: "write",
|
||||||
|
pull_requests: "write",
|
||||||
|
issues: "write",
|
||||||
|
actions: "read",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips empty lines", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS =
|
||||||
|
"actions: read\n\n\nworkflows: write\n\n";
|
||||||
|
expect(parseAdditionalPermissions()).toEqual({
|
||||||
|
contents: "write",
|
||||||
|
pull_requests: "write",
|
||||||
|
issues: "write",
|
||||||
|
actions: "read",
|
||||||
|
workflows: "write",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips lines without colons", () => {
|
||||||
|
process.env.ADDITIONAL_PERMISSIONS =
|
||||||
|
"actions: read\ninvalid line\nworkflows: write";
|
||||||
|
expect(parseAdditionalPermissions()).toEqual({
|
||||||
|
contents: "write",
|
||||||
|
pull_requests: "write",
|
||||||
|
issues: "write",
|
||||||
|
actions: "read",
|
||||||
|
workflows: "write",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user