- Add helpful hint about workflow permissions when OIDC token is not found - Include response body in app token exchange failure errors for better debugging 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
126 lines
3.5 KiB
TypeScript
126 lines
3.5 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
import * as core from "@actions/core";
|
|
|
|
type RetryOptions = {
|
|
maxAttempts?: number;
|
|
initialDelayMs?: number;
|
|
maxDelayMs?: number;
|
|
backoffFactor?: number;
|
|
};
|
|
|
|
async function retryWithBackoff<T>(
|
|
operation: () => Promise<T>,
|
|
options: RetryOptions = {},
|
|
): Promise<T> {
|
|
const {
|
|
maxAttempts = 3,
|
|
initialDelayMs = 5000,
|
|
maxDelayMs = 20000,
|
|
backoffFactor = 2,
|
|
} = options;
|
|
|
|
let delayMs = initialDelayMs;
|
|
let lastError: Error | undefined;
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
try {
|
|
console.log(`Attempt ${attempt} of ${maxAttempts}...`);
|
|
return await operation();
|
|
} catch (error) {
|
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
console.error(`Attempt ${attempt} failed:`, lastError.message);
|
|
|
|
if (attempt < maxAttempts) {
|
|
console.log(`Retrying in ${delayMs / 1000} seconds...`);
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
delayMs = Math.min(delayMs * backoffFactor, maxDelayMs);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.error(`Operation failed after ${maxAttempts} attempts`);
|
|
throw lastError;
|
|
}
|
|
|
|
async function getOidcToken(): Promise<string> {
|
|
try {
|
|
const oidcToken = await core.getIDToken("claude-code-github-action");
|
|
|
|
return oidcToken;
|
|
} catch (error) {
|
|
console.error("Failed to get OIDC token:", error);
|
|
throw new Error(
|
|
"Could not fetch an OIDC token. Did you remember to add `id-token: write` to your workflow permissions?",
|
|
);
|
|
}
|
|
}
|
|
|
|
async function exchangeForAppToken(oidcToken: string): Promise<string> {
|
|
const response = await fetch(
|
|
"https://api.anthropic.com/api/github/github-app-token-exchange",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${oidcToken}`,
|
|
},
|
|
},
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const responseJson = (await response.json()) as {
|
|
error?: {
|
|
message?: string;
|
|
};
|
|
};
|
|
console.error(
|
|
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson?.error?.message ?? "Unknown error"}`,
|
|
);
|
|
throw new Error(`${responseJson?.error?.message ?? "Unknown error"}`);
|
|
}
|
|
|
|
const appTokenData = (await response.json()) as {
|
|
token?: string;
|
|
app_token?: string;
|
|
};
|
|
const appToken = appTokenData.token || appTokenData.app_token;
|
|
|
|
if (!appToken) {
|
|
throw new Error("App token not found in response");
|
|
}
|
|
|
|
return appToken;
|
|
}
|
|
|
|
export async function setupGitHubToken(): Promise<string> {
|
|
try {
|
|
// Check if GitHub token was provided as override
|
|
const providedToken = process.env.OVERRIDE_GITHUB_TOKEN;
|
|
|
|
if (providedToken) {
|
|
console.log("Using provided GITHUB_TOKEN for authentication");
|
|
core.setOutput("GITHUB_TOKEN", providedToken);
|
|
return providedToken;
|
|
}
|
|
|
|
console.log("Requesting OIDC token...");
|
|
const oidcToken = await retryWithBackoff(() => getOidcToken());
|
|
console.log("OIDC token successfully obtained");
|
|
|
|
console.log("Exchanging OIDC token for app token...");
|
|
const appToken = await retryWithBackoff(() =>
|
|
exchangeForAppToken(oidcToken),
|
|
);
|
|
console.log("App token successfully obtained");
|
|
|
|
console.log("Using GITHUB_TOKEN from OIDC");
|
|
core.setOutput("GITHUB_TOKEN", appToken);
|
|
return appToken;
|
|
} catch (error) {
|
|
core.setFailed(
|
|
`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.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|