Compare commits

...

3 Commits

Author SHA1 Message Date
Ashwin Bhat
f9388bed59 feat: enable show_full_output in structured output tests
Add `show_full_output: true` to all five test jobs in the structured
output test workflow to surface full Claude output for easier debugging.

Co-Authored-By: Claude (fennec-v7-fast) <noreply@anthropic.com>
2026-01-27 19:34:31 -08:00
GitHub Actions
49046e0709 chore: bump Claude Code to 2.1.21 and Agent SDK to 0.2.21 2026-01-28 02:24:23 +00:00
Ashwin Bhat
32ac7269f2 Revert "Revert "feat: send additional_permissions in token exchange request (…" (#866)
This reverts commit 231bd75b71.
2026-01-27 14:35:18 -08:00
10 changed files with 172 additions and 18 deletions

View File

@@ -29,6 +29,7 @@ jobs:
- boolean_true: true
- boolean_false: false
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
show_full_output: true
claude_args: |
--allowedTools Bash
--json-schema '{"type":"object","properties":{"text_field":{"type":"string"},"number_field":{"type":"number"},"boolean_true":{"type":"boolean"},"boolean_false":{"type":"boolean"}},"required":["text_field","number_field","boolean_true","boolean_false"]}'
@@ -87,6 +88,7 @@ jobs:
- config: {"key": "value", "count": 3}
- empty_array: []
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
show_full_output: true
claude_args: |
--allowedTools Bash
--json-schema '{"type":"object","properties":{"items":{"type":"array","items":{"type":"string"}},"config":{"type":"object"},"empty_array":{"type":"array"}},"required":["items","config","empty_array"]}'
@@ -139,6 +141,7 @@ jobs:
- negative: -5
- decimal: 3.14
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
show_full_output: true
claude_args: |
--allowedTools Bash
--json-schema '{"type":"object","properties":{"zero":{"type":"number"},"empty_string":{"type":"string"},"negative":{"type":"number"},"decimal":{"type":"number"}},"required":["zero","empty_string","negative","decimal"]}'
@@ -193,6 +196,7 @@ jobs:
Run: echo "test"
Return EXACTLY: {test-result: "passed", item_count: 10}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
show_full_output: true
claude_args: |
--allowedTools Bash
--json-schema '{"type":"object","properties":{"test-result":{"type":"string"},"item_count":{"type":"number"}},"required":["test-result","item_count"]}'
@@ -231,6 +235,7 @@ jobs:
with:
prompt: "Run: echo 'complete'. Return: {done: true}"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
show_full_output: true
claude_args: |
--allowedTools Bash
--json-schema '{"type":"object","properties":{"done":{"type":"boolean"}},"required":["done"]}'

View File

@@ -223,7 +223,7 @@ runs:
# Install Claude Code if no custom executable is provided
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
CLAUDE_CODE_VERSION="2.1.20"
CLAUDE_CODE_VERSION="2.1.21"
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
for attempt in 1 2 3; do
echo "Installation attempt $attempt..."

View File

@@ -124,7 +124,7 @@ runs:
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
run: |
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
CLAUDE_CODE_VERSION="2.1.20"
CLAUDE_CODE_VERSION="2.1.21"
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
for attempt in 1 2 3; do
echo "Installation attempt $attempt..."

View File

@@ -6,7 +6,7 @@
"name": "@anthropic-ai/claude-code-base-action",
"dependencies": {
"@actions/core": "^1.10.1",
"@anthropic-ai/claude-agent-sdk": "^0.2.20",
"@anthropic-ai/claude-agent-sdk": "^0.2.21",
"shell-quote": "^1.8.3",
},
"devDependencies": {
@@ -27,7 +27,7 @@
"@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.20", "", { "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-Q2rJlYC2hEhJRKcOswJrcvm0O6H/uhXkRPAAqbAlFR/jbCWeg6jpyr9iUmVBFUFOBzAWqT2C6KLHiTJ8NySvQg=="],
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.21", "", { "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-aaeMZEkP1A8jaAGblpRYIGkrRYlDKT+oGznAyFiUT4zoutKqpNfd82bNBZ+T1B9Fz79rP3Q6Ws1xaMbOfkbJsA=="],
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],

View File

@@ -11,7 +11,7 @@
},
"dependencies": {
"@actions/core": "^1.10.1",
"@anthropic-ai/claude-agent-sdk": "^0.2.20",
"@anthropic-ai/claude-agent-sdk": "^0.2.21",
"shell-quote": "^1.8.3"
},
"devDependencies": {

View File

@@ -7,7 +7,7 @@
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.1",
"@anthropic-ai/claude-agent-sdk": "^0.2.20",
"@anthropic-ai/claude-agent-sdk": "^0.2.21",
"@modelcontextprotocol/sdk": "^1.11.0",
"@octokit/graphql": "^8.2.2",
"@octokit/rest": "^21.1.1",
@@ -37,7 +37,7 @@
"@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.20", "", { "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-Q2rJlYC2hEhJRKcOswJrcvm0O6H/uhXkRPAAqbAlFR/jbCWeg6jpyr9iUmVBFUFOBzAWqT2C6KLHiTJ8NySvQg=="],
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.21", "", { "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-aaeMZEkP1A8jaAGblpRYIGkrRYlDKT+oGznAyFiUT4zoutKqpNfd82bNBZ+T1B9Fz79rP3Q6Ws1xaMbOfkbJsA=="],
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],

View File

@@ -172,9 +172,14 @@ jobs:
**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
- 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

View File

@@ -12,7 +12,7 @@
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.1",
"@anthropic-ai/claude-agent-sdk": "^0.2.20",
"@anthropic-ai/claude-agent-sdk": "^0.2.21",
"@modelcontextprotocol/sdk": "^1.11.0",
"@octokit/graphql": "^8.2.2",
"@octokit/rest": "^21.1.1",

View File

@@ -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(
"https://api.anthropic.com/api/github/github-app-token-exchange",
{
method: "POST",
headers: {
Authorization: `Bearer ${oidcToken}`,
},
},
fetchOptions,
);
if (!response.ok) {
@@ -89,9 +134,11 @@ export async function setupGitHubToken(): Promise<string> {
const oidcToken = await retryWithBackoff(() => getOidcToken());
console.log("OIDC token successfully obtained");
const permissions = parseAdditionalPermissions();
console.log("Exchanging OIDC token for app token...");
const appToken = await retryWithBackoff(() =>
exchangeForAppToken(oidcToken),
exchangeForAppToken(oidcToken, permissions),
);
console.log("App token successfully obtained");

View 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",
});
});
});