diff --git a/docs/configuration.md b/docs/configuration.md index eb352b34..46c2687c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -172,14 +172,9 @@ jobs: **Important Notes**: -- The GitHub token must have the corresponding permission in your workflow +- The GitHub token must have the `actions: read` permission in your workflow - If the permission is missing, Claude will warn you and suggest adding it -- 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 +- Currently, only `actions: read` is supported, but the format allows for future extensions ## Custom Environment Variables diff --git a/src/github/token.ts b/src/github/token.ts index 54948d19..6cb9079c 100644 --- a/src/github/token.ts +++ b/src/github/token.ts @@ -16,60 +16,15 @@ async function getOidcToken(): Promise { } } -const DEFAULT_PERMISSIONS: Record = { - contents: "write", - pull_requests: "write", - issues: "write", -}; - -export function parseAdditionalPermissions(): - | Record - | undefined { - const raw = process.env.ADDITIONAL_PERMISSIONS; - if (!raw || !raw.trim()) { - return undefined; - } - - const additional: Record = {}; - 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, -): Promise { - const headers: Record = { - Authorization: `Bearer ${oidcToken}`, - }; - const fetchOptions: RequestInit = { - method: "POST", - headers, - }; - - if (permissions) { - headers["Content-Type"] = "application/json"; - fetchOptions.body = JSON.stringify({ permissions }); - } - +async function exchangeForAppToken(oidcToken: string): Promise { const response = await fetch( "https://api.anthropic.com/api/github/github-app-token-exchange", - fetchOptions, + { + method: "POST", + headers: { + Authorization: `Bearer ${oidcToken}`, + }, + }, ); if (!response.ok) { @@ -134,11 +89,9 @@ export async function setupGitHubToken(): Promise { 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, permissions), + exchangeForAppToken(oidcToken), ); console.log("App token successfully obtained"); diff --git a/test/parse-permissions.test.ts b/test/parse-permissions.test.ts deleted file mode 100644 index a13f9234..00000000 --- a/test/parse-permissions.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -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", - }); - }); -});