diff --git a/.github/workflows/audit-workflows.lock.yml b/.github/workflows/audit-workflows.lock.yml
index c83e4d6babc..640f42c29e1 100644
--- a/.github/workflows/audit-workflows.lock.yml
+++ b/.github/workflows/audit-workflows.lock.yml
@@ -1680,6 +1680,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1772,7 +1976,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(/tmp/gh-aw/jqschema.sh),Bash(cat),Bash(date),Bash(echo),Bash(grep),Bash(head),Bash(jq *),Bash(ls),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc),Bash(yq),BashOutput,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_sub_issues,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1790,6 +1994,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3776,6 +4058,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3802,7 +4288,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3811,6 +4297,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/blog-auditor.lock.yml b/.github/workflows/blog-auditor.lock.yml
index a6c56473aee..ba90eab1004 100644
--- a/.github/workflows/blog-auditor.lock.yml
+++ b/.github/workflows/blog-auditor.lock.yml
@@ -1584,6 +1584,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1699,7 +1903,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(cat *),Bash(cat),Bash(date *),Bash(date),Bash(echo *),Bash(echo),Bash(gh aw compile *),Bash(grep),Bash(head),Bash(ls *),Bash(ls),Bash(mktemp *),Bash(pwd),Bash(rm *),Bash(sort),Bash(tail),Bash(test *),Bash(uniq),Bash(wc),Bash(yq),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_sub_issues,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users,mcp__playwright__browser_click,mcp__playwright__browser_close,mcp__playwright__browser_console_messages,mcp__playwright__browser_drag,mcp__playwright__browser_evaluate,mcp__playwright__browser_file_upload,mcp__playwright__browser_fill_form,mcp__playwright__browser_handle_dialog,mcp__playwright__browser_hover,mcp__playwright__browser_install,mcp__playwright__browser_navigate,mcp__playwright__browser_navigate_back,mcp__playwright__browser_network_requests,mcp__playwright__browser_press_key,mcp__playwright__browser_resize,mcp__playwright__browser_select_option,mcp__playwright__browser_snapshot,mcp__playwright__browser_tabs,mcp__playwright__browser_take_screenshot,mcp__playwright__browser_type,mcp__playwright__browser_wait_for" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1716,6 +1920,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3754,6 +4036,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3780,7 +4266,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3789,6 +4275,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/changeset-generator.lock.yml b/.github/workflows/changeset-generator.lock.yml
index 5cc0206d958..03bfdec0bad 100644
--- a/.github/workflows/changeset-generator.lock.yml
+++ b/.github/workflows/changeset-generator.lock.yml
@@ -1967,6 +1967,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -2066,7 +2270,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(/tmp/gh-aw/jqschema.sh),Bash(cat),Bash(date),Bash(echo),Bash(git add:*),Bash(git branch:*),Bash(git checkout:*),Bash(git commit:*),Bash(git merge:*),Bash(git rm:*),Bash(git status),Bash(git switch:*),Bash(grep),Bash(head),Bash(jq *),Bash(ls),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc),Bash(yq),BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_sub_issues,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -2083,6 +2287,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3909,6 +4191,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3935,7 +4421,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3944,6 +4430,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/cli-version-checker.lock.yml b/.github/workflows/cli-version-checker.lock.yml
index 0e643920544..c30e8a17f11 100644
--- a/.github/workflows/cli-version-checker.lock.yml
+++ b/.github/workflows/cli-version-checker.lock.yml
@@ -1530,6 +1530,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1637,7 +1841,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --max-turns 75 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(/tmp/gh-aw/jqschema.sh),Bash(cat *),Bash(cat),Bash(claude-code --help),Bash(codex --help),Bash(copilot --help),Bash(date),Bash(echo),Bash(git *),Bash(grep *),Bash(grep),Bash(head),Bash(jq *),Bash(ls *),Bash(ls),Bash(make *),Bash(npm install *),Bash(npm list *),Bash(npm view *),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc),Bash(yq),BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,WebFetch,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_sub_issues,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1655,6 +1859,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3704,6 +3986,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3730,7 +4216,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --max-turns 75 --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3740,6 +4226,84 @@ jobs:
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
GH_AW_MAX_TURNS: 75
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/commit-changes-analyzer.lock.yml b/.github/workflows/commit-changes-analyzer.lock.yml
index da8e3506d85..327b52289f9 100644
--- a/.github/workflows/commit-changes-analyzer.lock.yml
+++ b/.github/workflows/commit-changes-analyzer.lock.yml
@@ -1512,6 +1512,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1590,7 +1794,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --max-turns 100 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash,BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_sub_issues,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1608,6 +1812,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3594,6 +3876,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3620,7 +4106,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --max-turns 100 --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3630,6 +4116,84 @@ jobs:
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
GH_AW_MAX_TURNS: 100
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/copilot-agent-analysis.lock.yml b/.github/workflows/copilot-agent-analysis.lock.yml
index b249afe2c3f..6c5e2bb3705 100644
--- a/.github/workflows/copilot-agent-analysis.lock.yml
+++ b/.github/workflows/copilot-agent-analysis.lock.yml
@@ -1829,6 +1829,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1880,7 +2084,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(/tmp/gh-aw/jqschema.sh),Bash(cat),Bash(date),Bash(echo),Bash(find .github -name '*.md'),Bash(find .github -type f -exec cat {} +),Bash(gh pr list *),Bash(gh search prs *),Bash(git diff),Bash(git log --oneline),Bash(grep),Bash(head),Bash(jq *),Bash(ls -la .github),Bash(ls),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc),Bash(yq),BashOutput,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__get_commit,mcp__github__get_file_contents,mcp__github__list_commits,mcp__github__list_pull_requests,mcp__github__pull_request_read,mcp__github__search_pull_requests" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1897,6 +2101,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3884,6 +4166,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3910,7 +4396,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3919,6 +4405,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml
index 2df621d0077..7eb30ae9c25 100644
--- a/.github/workflows/daily-doc-updater.lock.yml
+++ b/.github/workflows/daily-doc-updater.lock.yml
@@ -1496,6 +1496,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1554,7 +1758,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(cat),Bash(date),Bash(echo),Bash(find docs -name '*.md' -exec cat {} +),Bash(find docs -name '*.md' -o -name '*.mdx'),Bash(git add:*),Bash(git branch:*),Bash(git checkout:*),Bash(git commit:*),Bash(git merge:*),Bash(git rm:*),Bash(git status),Bash(git switch:*),Bash(grep -r '*' docs),Bash(grep),Bash(head),Bash(ls -la docs),Bash(ls),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc),Bash(yq),BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__get_commit,mcp__github__get_file_contents,mcp__github__list_commits,mcp__github__list_pull_requests,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_pull_requests" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1571,6 +1775,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3878,6 +4160,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3904,7 +4390,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3913,6 +4399,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/example-workflow-analyzer.lock.yml b/.github/workflows/example-workflow-analyzer.lock.yml
index 5d3410dbc81..011447758a8 100644
--- a/.github/workflows/example-workflow-analyzer.lock.yml
+++ b/.github/workflows/example-workflow-analyzer.lock.yml
@@ -1306,6 +1306,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1326,7 +1530,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "ExitPlanMode,Glob,Grep,LS,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__get_workflow_run,mcp__github__list_workflow_runs" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1343,6 +1547,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3381,6 +3663,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3407,7 +3893,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3416,6 +3902,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/github-mcp-tools-report.lock.yml b/.github/workflows/github-mcp-tools-report.lock.yml
index 11d9a736b97..5e5ac73bcd5 100644
--- a/.github/workflows/github-mcp-tools-report.lock.yml
+++ b/.github/workflows/github-mcp-tools-report.lock.yml
@@ -1719,6 +1719,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1820,7 +2024,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(cat),Bash(date),Bash(echo),Bash(git add:*),Bash(git branch:*),Bash(git checkout:*),Bash(git commit:*),Bash(git merge:*),Bash(git rm:*),Bash(git status),Bash(git switch:*),Bash(grep),Bash(head),Bash(ls),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc),Bash(yq),BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_sub_issues,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1837,6 +2041,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -4392,6 +4674,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -4418,7 +4904,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -4427,6 +4913,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/go-logger.lock.yml b/.github/workflows/go-logger.lock.yml
index 9b67ce8c2fb..86e055bab2a 100644
--- a/.github/workflows/go-logger.lock.yml
+++ b/.github/workflows/go-logger.lock.yml
@@ -1547,6 +1547,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1600,7 +1804,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(./gh-aw compile *),Bash(cat),Bash(date),Bash(echo),Bash(find pkg -name '*.go' -type f ! -name '*_test.go'),Bash(git add:*),Bash(git branch:*),Bash(git checkout:*),Bash(git commit:*),Bash(git merge:*),Bash(git rm:*),Bash(git status),Bash(git switch:*),Bash(grep -n 'func ' pkg/*.go),Bash(grep -r 'var log = logger.New' pkg --include='*.go'),Bash(grep),Bash(head -n * pkg/**/*.go),Bash(head),Bash(ls),Bash(make build),Bash(make recompile),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc -l pkg/**/*.go),Bash(wc),Bash(yq),BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__get_file_contents,mcp__github__search_code" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1617,6 +1821,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3913,6 +4195,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3939,7 +4425,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3948,6 +4434,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/go-pattern-detector.lock.yml b/.github/workflows/go-pattern-detector.lock.yml
index 2a5df67c590..88d907acbef 100644
--- a/.github/workflows/go-pattern-detector.lock.yml
+++ b/.github/workflows/go-pattern-detector.lock.yml
@@ -1422,6 +1422,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1495,7 +1699,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "ExitPlanMode,Glob,Grep,LS,NotebookRead,Read,Task,TodoWrite,Write,mcp__ast-grep,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_sub_issues,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1512,6 +1716,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3548,6 +3830,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3574,7 +4060,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3583,6 +4069,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/instructions-janitor.lock.yml b/.github/workflows/instructions-janitor.lock.yml
index 9fb169509e3..97e6a425518 100644
--- a/.github/workflows/instructions-janitor.lock.yml
+++ b/.github/workflows/instructions-janitor.lock.yml
@@ -1495,6 +1495,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1551,7 +1755,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(cat .github/instructions/github-agentic-workflows.instructions.md),Bash(cat),Bash(date),Bash(echo),Bash(git add:*),Bash(git branch:*),Bash(git checkout:*),Bash(git commit:*),Bash(git describe --tags --abbrev=0),Bash(git log --since='*' --pretty=format:'%h %s' -- docs/),Bash(git merge:*),Bash(git rm:*),Bash(git status),Bash(git switch:*),Bash(grep),Bash(head),Bash(ls),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc -l .github/instructions/github-agentic-workflows.instructions.md),Bash(wc),Bash(yq),BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__get_commit,mcp__github__get_file_contents,mcp__github__get_latest_release,mcp__github__list_commits,mcp__github__search_code" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1568,6 +1772,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3864,6 +4146,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3890,7 +4376,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3899,6 +4385,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/lockfile-stats.lock.yml b/.github/workflows/lockfile-stats.lock.yml
index 580e8437dce..8ee1c38ca6d 100644
--- a/.github/workflows/lockfile-stats.lock.yml
+++ b/.github/workflows/lockfile-stats.lock.yml
@@ -1673,6 +1673,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1763,7 +1967,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(cat),Bash(date),Bash(echo),Bash(grep),Bash(head),Bash(ls),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc),Bash(yq),BashOutput,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_sub_issues,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1780,6 +1984,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3766,6 +4048,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3792,7 +4278,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3801,6 +4287,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/schema-consistency-checker.lock.yml b/.github/workflows/schema-consistency-checker.lock.yml
index b0be07a2054..0ab29e274d4 100644
--- a/.github/workflows/schema-consistency-checker.lock.yml
+++ b/.github/workflows/schema-consistency-checker.lock.yml
@@ -1648,6 +1648,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1730,7 +1934,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash,BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_sub_issues,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1747,6 +1951,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3734,6 +4016,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3760,7 +4246,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3769,6 +4255,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/scout.lock.yml b/.github/workflows/scout.lock.yml
index f241e64449f..8fde5858c1a 100644
--- a/.github/workflows/scout.lock.yml
+++ b/.github/workflows/scout.lock.yml
@@ -2848,6 +2848,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -2954,7 +3158,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(/tmp/gh-aw/jqschema.sh),Bash(cat),Bash(date),Bash(echo),Bash(grep),Bash(head),Bash(jq *),Bash(ls),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc),Bash(yq),BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__arxiv__get_paper_details,mcp__arxiv__get_paper_pdf,mcp__arxiv__search_arxiv,mcp__context7__get-library-docs,mcp__context7__resolve-library-id,mcp__deepwiki__ask_question,mcp__deepwiki__read_wiki_contents,mcp__deepwiki__read_wiki_structure,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_sub_issues,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users,mcp__markitdown,mcp__microsoftdocs,mcp__tavily" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -2971,6 +3175,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -4733,6 +5015,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -4759,7 +5245,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -4768,6 +5254,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/security-fix-pr.lock.yml b/.github/workflows/security-fix-pr.lock.yml
index ce53e2e60bc..2bd24d61361 100644
--- a/.github/workflows/security-fix-pr.lock.yml
+++ b/.github/workflows/security-fix-pr.lock.yml
@@ -1464,6 +1464,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1497,7 +1701,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash,BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__get_code_scanning_alert,mcp__github__get_file_contents,mcp__github__get_pull_request,mcp__github__list_code_scanning_alerts,mcp__github__list_pull_requests" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1514,6 +1718,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3821,6 +4103,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3847,7 +4333,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3856,6 +4342,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/semantic-function-refactor.lock.yml b/.github/workflows/semantic-function-refactor.lock.yml
index 7a767f306e5..a12c9ec70e5 100644
--- a/.github/workflows/semantic-function-refactor.lock.yml
+++ b/.github/workflows/semantic-function-refactor.lock.yml
@@ -1722,6 +1722,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1768,7 +1972,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(cat pkg/**/*.go),Bash(cat),Bash(date),Bash(echo),Bash(find pkg -name '*.go' ! -name '*_test.go' -type f),Bash(find pkg -type f -name '*.go' ! -name '*_test.go'),Bash(grep -r 'func ' pkg --include='*.go'),Bash(grep),Bash(head -n * pkg/**/*.go),Bash(head),Bash(ls -la pkg/),Bash(ls -la pkg/workflow/),Bash(ls),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc -l pkg/**/*.go),Bash(wc),Bash(yq),BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__get_file_contents,mcp__github__search_code,mcp__serena" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1785,6 +1989,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3823,6 +4105,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3849,7 +4335,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3858,6 +4344,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml
index d8b0bbe9ce9..0334faa9094 100644
--- a/.github/workflows/smoke-claude.lock.yml
+++ b/.github/workflows/smoke-claude.lock.yml
@@ -1262,6 +1262,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1334,7 +1538,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "ExitPlanMode,Glob,Grep,LS,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_sub_issues,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1352,6 +1556,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3389,6 +3671,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3415,7 +3901,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -3424,6 +3910,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/smoke-detector.lock.yml b/.github/workflows/smoke-detector.lock.yml
index 91371bdcf54..1cb81a79c9d 100644
--- a/.github/workflows/smoke-detector.lock.yml
+++ b/.github/workflows/smoke-detector.lock.yml
@@ -1841,6 +1841,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -1917,7 +2121,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,LS,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_sub_issues,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -1934,6 +2138,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -3972,6 +4254,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -3998,7 +4484,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -4007,6 +4493,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml
index 5410f8b48a2..65d471b4bb2 100644
--- a/.github/workflows/technical-doc-writer.lock.yml
+++ b/.github/workflows/technical-doc-writer.lock.yml
@@ -2169,6 +2169,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -2227,7 +2431,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(cat),Bash(date),Bash(echo),Bash(find .github/workflows -name '*.md'),Bash(git add:*),Bash(git branch:*),Bash(git checkout:*),Bash(git commit:*),Bash(git merge:*),Bash(git rm:*),Bash(git status),Bash(git switch:*),Bash(grep),Bash(head),Bash(ls -la docs),Bash(ls),Bash(make*),Bash(npm ci),Bash(npm run*),Bash(pwd),Bash(sort),Bash(tail),Bash(uniq),Bash(wc),Bash(yq),BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__add_reaction,mcp__github__get_file_contents,mcp__github__get_issue,mcp__github__get_pull_request,mcp__github__list_commits,mcp__github__pull_request_read" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -2247,6 +2451,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -4561,6 +4843,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -4587,7 +5073,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -4596,6 +5082,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml
index 0f4e3cc8b2e..d647ab85cf0 100644
--- a/.github/workflows/unbloat-docs.lock.yml
+++ b/.github/workflows/unbloat-docs.lock.yml
@@ -2377,6 +2377,210 @@ jobs:
name: aw_info.json
path: /tmp/gh-aw/aw_info.json
if-no-files-found: warn
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -2465,7 +2669,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools "Bash(cat *),Bash(cat),Bash(cd *),Bash(cp *),Bash(curl *),Bash(date),Bash(echo),Bash(find docs -name '*.md'),Bash(git add:*),Bash(git branch:*),Bash(git checkout:*),Bash(git commit:*),Bash(git merge:*),Bash(git rm:*),Bash(git status),Bash(git switch:*),Bash(grep -n *),Bash(grep),Bash(head *),Bash(head),Bash(kill *),Bash(ls),Bash(mkdir *),Bash(mv *),Bash(node *),Bash(ps *),Bash(pwd),Bash(sleep *),Bash(sort),Bash(tail *),Bash(tail),Bash(uniq),Bash(wc -l *),Bash(wc),Bash(yq),BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__get_file_contents,mcp__github__get_pull_request,mcp__github__get_repository,mcp__github__list_commits,mcp__github__search_pull_requests,mcp__playwright__browser_click,mcp__playwright__browser_close,mcp__playwright__browser_console_messages,mcp__playwright__browser_drag,mcp__playwright__browser_evaluate,mcp__playwright__browser_file_upload,mcp__playwright__browser_fill_form,mcp__playwright__browser_handle_dialog,mcp__playwright__browser_hover,mcp__playwright__browser_install,mcp__playwright__browser_navigate,mcp__playwright__browser_navigate_back,mcp__playwright__browser_network_requests,mcp__playwright__browser_press_key,mcp__playwright__browser_resize,mcp__playwright__browser_select_option,mcp__playwright__browser_snapshot,mcp__playwright__browser_tabs,mcp__playwright__browser_take_screenshot,mcp__playwright__browser_type,mcp__playwright__browser_wait_for" --debug --verbose --permission-mode bypassPermissions --output-format stream-json --settings /tmp/gh-aw/.claude/settings.json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/agent-stdio.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -2485,6 +2689,84 @@ jobs:
rm -rf .claude/hooks/network_permissions.py || true
rm -rf .claude/hooks || true
rm -rf .claude || true
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Redact secrets in logs
if: always()
uses: actions/github-script@v8
@@ -4798,6 +5080,210 @@ jobs:
node-version: '24'
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code@2.0.25
+ - name: Setup OIDC token
+ id: setup_oidc_token
+ if: secrets.ANTHROPIC_API_KEY != ''
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_AUDIENCE: claude-code-github-action
+ GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange
+ GH_AW_OIDC_OAUTH_TOKEN: CLAUDE_CODE_OAUTH_TOKEN
+ GH_AW_OIDC_API_KEY: ANTHROPIC_API_KEY
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ with:
+ script: |
+ async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+
+ let lastError;
+
+ for (let i = 0; i < maxRetries; i++) {
+
+ try {
+
+ return await fn();
+
+ } catch (error) {
+
+ lastError = error;
+
+ if (i < maxRetries - 1) {
+
+ const delay = initialDelay * Math.pow(2, i);
+
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+
+ await new Promise(resolve => setTimeout(resolve, delay));
+
+ }
+
+ }
+
+ }
+
+ throw lastError;
+
+ }
+
+ async function getOidcToken(audience) {
+
+ try {
+
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+
+ const oidcToken = await core.getIDToken(audience);
+
+ core.info("OIDC token successfully obtained");
+
+ return oidcToken;
+
+ } catch (error) {
+
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(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, exchangeUrl) {
+
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${oidcToken}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ let responseJson;
+
+ try {
+
+ responseJson = await response.json();
+
+ } catch {
+
+ responseJson = {};
+
+ }
+
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+
+ core.info(
+
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+
+ );
+
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+
+ return;
+
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+
+ throw new Error(errorMessage);
+
+ }
+
+ const appTokenData = await response.json();
+
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+
+ throw new Error("App token not found in response");
+
+ }
+
+ core.info("App token successfully obtained");
+
+ return appToken;
+
+ }
+
+ async function main() {
+
+ try {
+
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+
+ return;
+
+ }
+
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+
+ core.setOutput("token", apiToken);
+
+ core.setOutput("token_source", "api_token");
+
+ core.exportVariable(apiTokenEnvVar, apiToken);
+
+ return;
+
+ }
+
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+
+ core.setOutput("token", oauthToken);
+
+ core.setOutput("token_source", "oauth");
+
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ core.setOutput("oidc_token_obtained", "true");
+
+ } catch (error) {
+
+ core.setFailed(
+
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+
+ );
+
+ }
+
+ }
+
+ await main();
+
- name: Execute Claude Code CLI
id: agentic_execution
# Allowed tools (sorted):
@@ -4824,7 +5310,7 @@ jobs:
# Execute Claude Code CLI with prompt from file
claude --print --allowed-tools "Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite" --debug --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log
env:
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_BUG_COMMAND: "1"
@@ -4833,6 +5319,84 @@ jobs:
MCP_TOOL_TIMEOUT: "60000"
BASH_DEFAULT_TIMEOUT_MS: "60000"
BASH_MAX_TIMEOUT_MS: "60000"
+ - name: Revoke OIDC token
+ id: revoke_oidc_token
+ if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'
+ uses: actions/github-script@v8
+ env:
+ GH_AW_OIDC_REVOKE_URL: https://api.anthropic.com/api/github/github-app-token-revoke
+ GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}
+ GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}
+ with:
+ script: |
+ async function main() {
+
+ try {
+
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ if (tokenObtained !== "true") {
+
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+
+ return;
+
+ }
+
+ if (!revokeUrl) {
+
+ core.info("No token revoke URL configured, skipping revocation");
+
+ return;
+
+ }
+
+ if (!token) {
+
+ core.warning("No token available for revocation");
+
+ return;
+
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+
+ method: "POST",
+
+ headers: {
+
+ Authorization: `Bearer ${token}`,
+
+ },
+
+ });
+
+ if (!response.ok) {
+
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+
+ return;
+
+ }
+
+ core.info("Token successfully revoked");
+
+ } catch (error) {
+
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+
+ }
+
+ }
+
+ await main();
+
- name: Parse threat detection results
uses: actions/github-script@v8
with:
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index 61c3401e6a3..dcc302c95c6 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -2974,6 +2974,34 @@
"type": "string"
},
"description": "Optional array of command-line arguments to pass to the AI engine CLI. These arguments are injected after all other args but before the prompt."
+ },
+ "oidc": {
+ "type": "object",
+ "description": "OpenID Connect authentication configuration for agentic engines. When configured, the workflow will use OIDC to obtain tokens with PAT fallback support.",
+ "properties": {
+ "audience": {
+ "type": "string",
+ "description": "OIDC audience identifier (e.g., 'claude-code-github-action'). Defaults to engine-specific audience if not specified."
+ },
+ "token_exchange_url": {
+ "type": "string",
+ "description": "URL endpoint to exchange OIDC token for an app token (required for OIDC authentication)"
+ },
+ "token_revoke_url": {
+ "type": "string",
+ "description": "URL endpoint to revoke the app token after workflow execution (optional)"
+ },
+ "oauth-token-env-var": {
+ "type": "string",
+ "description": "Environment variable name for OAuth token obtained via OIDC. For Claude: CLAUDE_CODE_OAUTH_TOKEN. Defaults to engine-specific variable."
+ },
+ "api-token-env-var": {
+ "type": "string",
+ "description": "Environment variable name for API key used as fallback. For Claude: ANTHROPIC_API_KEY. Defaults to engine-specific variable."
+ }
+ },
+ "required": ["token_exchange_url"],
+ "additionalProperties": false
}
},
"required": [
diff --git a/pkg/workflow/agentic_engine.go b/pkg/workflow/agentic_engine.go
index 02e3ace50d2..dd461f609dc 100644
--- a/pkg/workflow/agentic_engine.go
+++ b/pkg/workflow/agentic_engine.go
@@ -70,6 +70,21 @@ type CodingAgentEngine interface {
// GetVersionCommand returns the command to get the version of the agent (e.g., "copilot --version")
// Returns empty string if the engine does not support version reporting
GetVersionCommand() string
+
+ // GetOIDCConfig returns the OIDC configuration for this engine
+ // Returns nil if the engine does not support OIDC or OIDC is not configured
+ GetOIDCConfig(workflowData *WorkflowData) *OIDCConfig
+
+ // GetTokenEnvVarName returns the environment variable name for API key authentication tokens
+ // For Claude: ANTHROPIC_API_KEY (used as fallback when OIDC fails)
+ // For Copilot: GITHUB_TOKEN
+ // For Codex: OPENAI_API_KEY
+ GetTokenEnvVarName() string
+
+ // GetOAuthTokenEnvVarName returns the environment variable name for OAuth tokens obtained via OIDC
+ // For Claude: CLAUDE_CODE_OAUTH_TOKEN
+ // For other engines: typically same as GetTokenEnvVarName()
+ GetOAuthTokenEnvVarName() string
}
// ErrorPattern represents a regex pattern for extracting error information from logs
@@ -155,6 +170,35 @@ func (e *BaseEngine) GetVersionCommand() string {
return ""
}
+// GetOIDCConfig returns nil by default (engines can override for OIDC support)
+func (e *BaseEngine) GetOIDCConfig(workflowData *WorkflowData) *OIDCConfig {
+ return nil
+}
+
+// GetOIDCConfigWithDefault returns OIDC config from workflow data or falls back to default
+// This helper method allows engines to provide default OIDC configurations
+func (e *BaseEngine) GetOIDCConfigWithDefault(workflowData *WorkflowData, defaultConfig *OIDCConfig) *OIDCConfig {
+ // If explicit OIDC config is provided, use it
+ if workflowData.EngineConfig != nil && workflowData.EngineConfig.OIDC != nil && workflowData.EngineConfig.OIDC.TokenExchangeURL != "" {
+ return workflowData.EngineConfig.OIDC
+ }
+
+ // Return default OIDC configuration
+ return defaultConfig
+}
+
+// GetTokenEnvVarName returns the default API token environment variable name
+// Engines should override this to return engine-specific values
+func (e *BaseEngine) GetTokenEnvVarName() string {
+ return "GITHUB_TOKEN"
+}
+
+// GetOAuthTokenEnvVarName returns the default OAuth token environment variable name
+// By default, uses the same as API token. Engines can override for different OAuth token variables.
+func (e *BaseEngine) GetOAuthTokenEnvVarName() string {
+ return e.GetTokenEnvVarName()
+}
+
// GetLogFileForParsing returns the default log file path for parsing
// Engines can override this to use engine-specific log files
func (e *BaseEngine) GetLogFileForParsing() string {
diff --git a/pkg/workflow/claude_engine.go b/pkg/workflow/claude_engine.go
index cccaa8a8312..1ab89890b81 100644
--- a/pkg/workflow/claude_engine.go
+++ b/pkg/workflow/claude_engine.go
@@ -12,8 +12,11 @@ import (
)
// ClaudeEngine represents the Claude Code agentic engine
+// ClaudeEngine represents the Claude Code engine
type ClaudeEngine struct {
BaseEngine
+ // defaultOIDCConfig stores the default OIDC configuration for Claude
+ defaultOIDCConfig *OIDCConfig
}
func NewClaudeEngine() *ClaudeEngine {
@@ -29,6 +32,11 @@ func NewClaudeEngine() *ClaudeEngine {
supportsWebFetch: true, // Claude has built-in WebFetch support
supportsWebSearch: true, // Claude has built-in WebSearch support
},
+ defaultOIDCConfig: &OIDCConfig{
+ Audience: "claude-code-github-action",
+ TokenExchangeURL: "https://api.anthropic.com/api/github/github-app-token-exchange",
+ TokenRevokeURL: "https://api.anthropic.com/api/github/github-app-token-revoke",
+ },
}
}
@@ -83,11 +91,36 @@ func (e *ClaudeEngine) GetVersionCommand() string {
return "claude --version"
}
+// GetOIDCConfig returns the OIDC configuration for Claude engine
+// Claude has OIDC enabled by default with Anthropic's token exchange endpoint
+func (e *ClaudeEngine) GetOIDCConfig(workflowData *WorkflowData) *OIDCConfig {
+ return e.BaseEngine.GetOIDCConfigWithDefault(workflowData, e.defaultOIDCConfig)
+}
+
+// GetTokenEnvVarName returns the environment variable name for Claude's API key authentication
+// This is used as the fallback when OIDC authentication is not available
+func (e *ClaudeEngine) GetTokenEnvVarName() string {
+ return "ANTHROPIC_API_KEY"
+}
+
+// GetOAuthTokenEnvVarName returns the environment variable name for Claude's OAuth token
+// This is used for OIDC-obtained tokens
+func (e *ClaudeEngine) GetOAuthTokenEnvVarName() string {
+ return "CLAUDE_CODE_OAUTH_TOKEN"
+}
+
// GetExecutionSteps returns the GitHub Actions steps for executing Claude
func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile string) []GitHubActionStep {
// Handle custom steps if they exist in engine config
steps := InjectCustomEngineSteps(workflowData, e.convertStepToYAML)
+ // Add OIDC setup step - Claude has OIDC enabled by default
+ oidcConfig := e.GetOIDCConfig(workflowData)
+ if oidcConfig != nil {
+ oidcSetupStep := GenerateOIDCSetupStep(oidcConfig, e)
+ steps = append(steps, oidcSetupStep)
+ }
+
// Build claude CLI arguments based on configuration
var claudeArgs []string
@@ -190,8 +223,14 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str
// Add environment section - always include environment section for GH_AW_PROMPT
stepLines = append(stepLines, " env:")
- // Add Anthropic API key
- stepLines = append(stepLines, " ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}")
+ // Add authentication token - if OIDC is configured, use OAuth token from setup step OR fall back to API key
+ // The OIDC setup step outputs the token regardless of whether it came from OIDC or API key fallback
+ // We need to set ANTHROPIC_API_KEY to either the OIDC token OR the secret API key
+ if oidcConfig != nil {
+ stepLines = append(stepLines, " ANTHROPIC_API_KEY: ${{ steps.setup_oidc_token.outputs.token || secrets.ANTHROPIC_API_KEY }}")
+ } else {
+ stepLines = append(stepLines, " ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}")
+ }
// Disable telemetry, error reporting, and bug command for privacy and security
stepLines = append(stepLines, " DISABLE_TELEMETRY: \"1\"")
@@ -268,6 +307,12 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str
steps = append(steps, cleanupStep)
}
+ // Add OIDC revoke step - Claude has OIDC enabled by default
+ if oidcConfig != nil {
+ oidcRevokeStep := GenerateOIDCRevokeStep(oidcConfig)
+ steps = append(steps, oidcRevokeStep)
+ }
+
return steps
}
diff --git a/pkg/workflow/codex_engine.go b/pkg/workflow/codex_engine.go
index b25b2974c9e..16afc4433bb 100644
--- a/pkg/workflow/codex_engine.go
+++ b/pkg/workflow/codex_engine.go
@@ -82,6 +82,11 @@ func (e *CodexEngine) GetVersionCommand() string {
return "codex --version"
}
+// GetTokenEnvVarName returns the environment variable name for Codex's authentication token
+func (e *CodexEngine) GetTokenEnvVarName() string {
+ return "OPENAI_API_KEY"
+}
+
// GetDeclaredOutputFiles returns the output files that Codex may produce
// Codex (written in Rust) writes logs to ~/.codex/log/codex-tui.log
func (e *CodexEngine) GetDeclaredOutputFiles() []string {
diff --git a/pkg/workflow/copilot_engine.go b/pkg/workflow/copilot_engine.go
index c7f31506a8c..94ad794bebc 100644
--- a/pkg/workflow/copilot_engine.go
+++ b/pkg/workflow/copilot_engine.go
@@ -97,6 +97,11 @@ func (e *CopilotEngine) GetVersionCommand() string {
return "copilot --version"
}
+// GetTokenEnvVarName returns the environment variable name for Copilot's authentication token
+func (e *CopilotEngine) GetTokenEnvVarName() string {
+ return "GITHUB_TOKEN"
+}
+
// extractAddDirPaths extracts all directory paths from copilot args that follow --add-dir flags
func extractAddDirPaths(args []string) []string {
var dirs []string
diff --git a/pkg/workflow/engine.go b/pkg/workflow/engine.go
index b49fd5818c6..554e3f9801a 100644
--- a/pkg/workflow/engine.go
+++ b/pkg/workflow/engine.go
@@ -23,6 +23,7 @@ type EngineConfig struct {
Config string
Args []string
Firewall *FirewallConfig // AWF firewall configuration
+ OIDC *OIDCConfig // OIDC authentication configuration
}
// NetworkPermissions represents network access permissions
@@ -253,6 +254,12 @@ func (c *Compiler) ExtractEngineConfig(frontmatter map[string]any) (string, *Eng
}
}
+ // Extract optional 'oidc' field (object format)
+ config.OIDC = ParseOIDCConfig(engineObj)
+ if config.OIDC != nil {
+ engineLog.Print("Extracted OIDC configuration")
+ }
+
// Return the ID as the engineSetting for backwards compatibility
engineLog.Printf("Extracted engine configuration: ID=%s", config.ID)
return config.ID, config
diff --git a/pkg/workflow/js.go b/pkg/workflow/js.go
index d4731a230f7..8e145045902 100644
--- a/pkg/workflow/js.go
+++ b/pkg/workflow/js.go
@@ -87,6 +87,12 @@ var redactSecretsScript string
//go:embed js/notify_comment_error.cjs
var notifyCommentErrorScript string
+//go:embed js/setup_oidc_token.cjs
+var setupOIDCTokenScript string
+
+//go:embed js/revoke_oidc_token.cjs
+var revokeOIDCTokenScript string
+
// removeJavaScriptComments removes JavaScript comments (// and /* */) from code
// while preserving comments that appear within string literals
func removeJavaScriptComments(code string) string {
diff --git a/pkg/workflow/js/revoke_oidc_token.cjs b/pkg/workflow/js/revoke_oidc_token.cjs
new file mode 100644
index 00000000000..4470382b1e6
--- /dev/null
+++ b/pkg/workflow/js/revoke_oidc_token.cjs
@@ -0,0 +1,60 @@
+// @ts-check
+///
+
+/**
+ * Revoke OIDC token
+ *
+ * This script revokes the app token that was obtained via OIDC token exchange.
+ * It only runs if a token was obtained via OIDC (not fallback).
+ */
+
+/**
+ * Main function to revoke OIDC token
+ */
+async function main() {
+ try {
+ // Get configuration from environment variables
+ const revokeUrl = process.env.GH_AW_OIDC_REVOKE_URL;
+ const tokenObtained = process.env.GH_AW_OIDC_TOKEN_OBTAINED;
+ const token = process.env.GH_AW_OIDC_TOKEN;
+
+ // Only revoke if we obtained a token via OIDC
+ if (tokenObtained !== "true") {
+ core.info("No OIDC token to revoke (token from fallback or not obtained)");
+ return;
+ }
+
+ // If no revoke URL is configured, skip revocation
+ if (!revokeUrl) {
+ core.info("No token revoke URL configured, skipping revocation");
+ return;
+ }
+
+ if (!token) {
+ core.warning("No token available for revocation");
+ return;
+ }
+
+ core.info(`Revoking token at: ${revokeUrl}`);
+
+ const response = await fetch(revokeUrl, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (!response.ok) {
+ // Log warning but don't fail the workflow
+ core.warning(`Token revocation failed: ${response.status} ${response.statusText}`);
+ return;
+ }
+
+ core.info("Token successfully revoked");
+ } catch (error) {
+ // Log warning but don't fail the workflow for revocation errors
+ core.warning(`Failed to revoke token: ${error instanceof Error ? error.message : String(error)}`);
+ }
+}
+
+await main();
diff --git a/pkg/workflow/js/setup_oidc_token.cjs b/pkg/workflow/js/setup_oidc_token.cjs
new file mode 100644
index 00000000000..cba8ea8cde3
--- /dev/null
+++ b/pkg/workflow/js/setup_oidc_token.cjs
@@ -0,0 +1,162 @@
+// @ts-check
+///
+
+/**
+ * Setup OIDC token with PAT fallback
+ *
+ * This script attempts to:
+ * 1. Use an existing token if available in the fallback environment variable
+ * 2. Get an OIDC token using core.getIDToken()
+ * 3. Exchange the OIDC token for an app token
+ * 4. Set the token in the environment for the agentic engine
+ *
+ * Based on: https://github.com/anthropics/claude-code-action/blob/f30f5eecfce2f34fa72e40fa5f7bcdbdcad12eb8/src/github/token.ts
+ */
+
+/**
+ * Retry a function with exponential backoff
+ * @template T
+ * @param {() => Promise} fn - Function to retry
+ * @param {number} [maxRetries=3] - Maximum number of retries
+ * @param {number} [initialDelay=1000] - Initial delay in milliseconds
+ * @returns {Promise}
+ */
+async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
+ let lastError;
+ for (let i = 0; i < maxRetries; i++) {
+ try {
+ return await fn();
+ } catch (error) {
+ lastError = error;
+ if (i < maxRetries - 1) {
+ const delay = initialDelay * Math.pow(2, i);
+ core.info(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
+ await new Promise(resolve => setTimeout(resolve, delay));
+ }
+ }
+ }
+ throw lastError;
+}
+
+/**
+ * Get OIDC token from GitHub Actions
+ * @param {string} audience - OIDC audience identifier
+ * @returns {Promise}
+ */
+async function getOidcToken(audience) {
+ try {
+ core.info(`Requesting OIDC token with audience: ${audience}`);
+ const oidcToken = await core.getIDToken(audience);
+ core.info("OIDC token successfully obtained");
+ return oidcToken;
+ } catch (error) {
+ core.error(`Failed to get OIDC token: ${error instanceof Error ? error.message : String(error)}`);
+ throw new Error("Could not fetch an OIDC token. Did you remember to add `id-token: write` to your workflow permissions?");
+ }
+}
+
+/**
+ * Exchange OIDC token for app token
+ * @param {string} oidcToken - OIDC token from GitHub Actions
+ * @param {string} exchangeUrl - Token exchange URL
+ * @returns {Promise}
+ */
+async function exchangeForAppToken(oidcToken, exchangeUrl) {
+ core.info(`Exchanging OIDC token at: ${exchangeUrl}`);
+
+ const response = await fetch(exchangeUrl, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${oidcToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ /** @type {{ error?: { message?: string; details?: { error_code?: string } }; type?: string; message?: string }} */
+ let responseJson;
+ try {
+ responseJson = await response.json();
+ } catch {
+ responseJson = {};
+ }
+
+ // Check for specific workflow validation error codes that should skip the action
+ const errorCode = responseJson.error?.details?.error_code;
+
+ if (errorCode === "workflow_not_found_on_default_branch") {
+ const message = responseJson.message ?? responseJson.error?.message ?? "Workflow validation failed";
+ core.warning(`Skipping action due to workflow validation: ${message}`);
+ core.info(
+ "Action skipped due to workflow validation error. This is expected when adding 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."
+ );
+ core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
+ return;
+ }
+
+ const errorMessage = responseJson?.error?.message ?? "Unknown error";
+ core.error(`App token exchange failed: ${response.status} ${response.statusText} - ${errorMessage}`);
+ throw new Error(errorMessage);
+ }
+
+ /** @type {{ token?: string; app_token?: string }} */
+ const appTokenData = await response.json();
+ const appToken = appTokenData.token || appTokenData.app_token;
+
+ if (!appToken) {
+ throw new Error("App token not found in response");
+ }
+
+ core.info("App token successfully obtained");
+ return appToken;
+}
+
+/**
+ * Main function to setup OIDC token
+ */
+async function main() {
+ try {
+ // Get configuration from environment variables
+ const audience = process.env.GH_AW_OIDC_AUDIENCE;
+ const exchangeUrl = process.env.GH_AW_OIDC_EXCHANGE_URL;
+ const oauthTokenEnvVar = process.env.GH_AW_OIDC_OAUTH_TOKEN;
+ const apiTokenEnvVar = process.env.GH_AW_OIDC_API_KEY;
+
+ if (!audience || !exchangeUrl || !oauthTokenEnvVar || !apiTokenEnvVar) {
+ core.setFailed("Missing required OIDC configuration (audience, exchange_url, oauth_token, or api_key)");
+ return;
+ }
+
+ // Check if API token was provided as fallback
+ const apiToken = process.env[apiTokenEnvVar];
+
+ if (apiToken) {
+ core.info(`Using provided API token from ${apiTokenEnvVar} for authentication`);
+ core.setOutput("token", apiToken);
+ core.setOutput("token_source", "api_token");
+ core.exportVariable(apiTokenEnvVar, apiToken);
+ return;
+ }
+
+ // Get OIDC token with retry
+ const oidcToken = await retryWithBackoff(() => getOidcToken(audience));
+
+ // Exchange OIDC token for OAuth app token with retry
+ const oauthToken = await retryWithBackoff(() => exchangeForAppToken(oidcToken, exchangeUrl));
+
+ // Set the OAuth token in the environment for subsequent steps
+ core.info(`Setting OAuth token in environment variable: ${oauthTokenEnvVar}`);
+ core.setOutput("token", oauthToken);
+ core.setOutput("token_source", "oauth");
+ core.exportVariable(oauthTokenEnvVar, oauthToken);
+
+ // Also output the token for post-step revocation
+ core.setOutput("oidc_token_obtained", "true");
+ } catch (error) {
+ // Only set failed if we get here - workflow validation errors will return before this
+ core.setFailed(
+ `Failed to setup token: ${error instanceof Error ? error.message : String(error)}\n\nIf you instead wish to use an API token, provide it via the ${apiTokenEnvVar} secret.`
+ );
+ }
+}
+
+await main();
diff --git a/pkg/workflow/openid.go b/pkg/workflow/openid.go
new file mode 100644
index 00000000000..9666cd68041
--- /dev/null
+++ b/pkg/workflow/openid.go
@@ -0,0 +1,184 @@
+package workflow
+
+import "fmt"
+
+// OIDCConfig represents OpenID Connect authentication configuration for agentic engines
+type OIDCConfig struct {
+ // Audience is the OIDC audience identifier (e.g., "claude-code-github-action")
+ Audience string `yaml:"audience,omitempty"`
+
+ // TokenExchangeURL is the URL to exchange OIDC token for an app token
+ TokenExchangeURL string `yaml:"token_exchange_url,omitempty"`
+
+ // TokenRevokeURL is the URL to revoke the app token (optional)
+ TokenRevokeURL string `yaml:"token_revoke_url,omitempty"`
+
+ // OauthTokenEnvVar is the environment variable name for the OAuth token obtained via OIDC
+ // For Claude: CLAUDE_CODE_OAUTH_TOKEN
+ OauthTokenEnvVar string `yaml:"oauth-token-env-var,omitempty"`
+
+ // ApiTokenEnvVar is the environment variable name for the API token used as fallback
+ // For Claude: ANTHROPIC_API_KEY
+ ApiTokenEnvVar string `yaml:"api-token-env-var,omitempty"`
+}
+
+// ParseOIDCConfig parses OIDC configuration from engine object
+func ParseOIDCConfig(engineObj map[string]any) *OIDCConfig {
+ oidc, hasOIDC := engineObj["oidc"]
+ if !hasOIDC {
+ return nil
+ }
+
+ oidcObj, ok := oidc.(map[string]any)
+ if !ok {
+ return nil
+ }
+
+ oidcConfig := &OIDCConfig{}
+
+ // Extract audience field
+ if audience, hasAudience := oidcObj["audience"]; hasAudience {
+ if audienceStr, ok := audience.(string); ok {
+ oidcConfig.Audience = audienceStr
+ }
+ }
+
+ // Extract token_exchange_url field
+ if tokenExchangeURL, hasTokenExchangeURL := oidcObj["token_exchange_url"]; hasTokenExchangeURL {
+ if tokenExchangeURLStr, ok := tokenExchangeURL.(string); ok {
+ oidcConfig.TokenExchangeURL = tokenExchangeURLStr
+ }
+ }
+
+ // Extract token_revoke_url field (optional)
+ if tokenRevokeURL, hasTokenRevokeURL := oidcObj["token_revoke_url"]; hasTokenRevokeURL {
+ if tokenRevokeURLStr, ok := tokenRevokeURL.(string); ok {
+ oidcConfig.TokenRevokeURL = tokenRevokeURLStr
+ }
+ }
+
+ // Extract oauth-token-env-var field (optional)
+ if oauthTokenEnvVar, hasOauthTokenEnvVar := oidcObj["oauth-token-env-var"]; hasOauthTokenEnvVar {
+ if oauthTokenEnvVarStr, ok := oauthTokenEnvVar.(string); ok {
+ oidcConfig.OauthTokenEnvVar = oauthTokenEnvVarStr
+ }
+ }
+
+ // Extract api-token-env-var field (optional)
+ if apiTokenEnvVar, hasApiTokenEnvVar := oidcObj["api-token-env-var"]; hasApiTokenEnvVar {
+ if apiTokenEnvVarStr, ok := apiTokenEnvVar.(string); ok {
+ oidcConfig.ApiTokenEnvVar = apiTokenEnvVarStr
+ }
+ }
+
+ return oidcConfig
+}
+
+// HasOIDCConfig checks if the engine has OIDC configuration
+// OIDC is considered enabled if token_exchange_url is present
+func HasOIDCConfig(config *EngineConfig) bool {
+ return config != nil && config.OIDC != nil && config.OIDC.TokenExchangeURL != ""
+}
+
+// GetOIDCAudience returns the OIDC audience identifier, with a default based on engine
+func (config *OIDCConfig) GetOIDCAudience(engineID string) string {
+ if config.Audience != "" {
+ return config.Audience
+ }
+
+ // Default audiences based on engine
+ switch engineID {
+ case "claude":
+ return "claude-code-github-action"
+ default:
+ return ""
+ }
+}
+
+// GetOAuthTokenEnvVar returns the OAuth token environment variable, falling back to engine default
+func (config *OIDCConfig) GetOAuthTokenEnvVar(engine CodingAgentEngine) string {
+ if config.OauthTokenEnvVar != "" {
+ return config.OauthTokenEnvVar
+ }
+ return engine.GetOAuthTokenEnvVarName()
+}
+
+// GetApiTokenEnvVar returns the API token environment variable, falling back to engine default
+func (config *OIDCConfig) GetApiTokenEnvVar(engine CodingAgentEngine) string {
+ if config.ApiTokenEnvVar != "" {
+ return config.ApiTokenEnvVar
+ }
+ return engine.GetTokenEnvVarName()
+}
+
+// GenerateOIDCSetupStep generates a GitHub Actions step to setup OIDC token
+func GenerateOIDCSetupStep(oidcConfig *OIDCConfig, engine CodingAgentEngine) GitHubActionStep {
+ var stepLines []string
+
+ stepLines = append(stepLines, " - name: Setup OIDC token")
+ stepLines = append(stepLines, " id: setup_oidc_token")
+ // Only run if the fallback API token secret exists (check for non-empty secret)
+ apiTokenEnvVar := oidcConfig.GetApiTokenEnvVar(engine)
+ stepLines = append(stepLines, fmt.Sprintf(" if: secrets.%s != ''", apiTokenEnvVar))
+ stepLines = append(stepLines, " uses: actions/github-script@v8")
+ stepLines = append(stepLines, " env:")
+
+ // Add OIDC configuration as environment variables
+ audience := oidcConfig.GetOIDCAudience(engine.GetID())
+ if audience != "" {
+ stepLines = append(stepLines, fmt.Sprintf(" GH_AW_OIDC_AUDIENCE: %s", audience))
+ }
+
+ if oidcConfig.TokenExchangeURL != "" {
+ stepLines = append(stepLines, fmt.Sprintf(" GH_AW_OIDC_EXCHANGE_URL: %s", oidcConfig.TokenExchangeURL))
+ }
+
+ // OAuth token environment variable (where OIDC token will be stored)
+ oauthTokenEnvVar := oidcConfig.GetOAuthTokenEnvVar(engine)
+ stepLines = append(stepLines, fmt.Sprintf(" GH_AW_OIDC_OAUTH_TOKEN: %s", oauthTokenEnvVar))
+
+ // API token environment variable (fallback) - already declared above
+ stepLines = append(stepLines, fmt.Sprintf(" GH_AW_OIDC_API_KEY: %s", apiTokenEnvVar))
+ // Add the actual fallback secret if it exists
+ stepLines = append(stepLines, fmt.Sprintf(" %s: ${{ secrets.%s }}", apiTokenEnvVar, apiTokenEnvVar))
+
+ stepLines = append(stepLines, " with:")
+ stepLines = append(stepLines, " script: |")
+
+ // Add the JavaScript script with proper indentation
+ formattedScript := FormatJavaScriptForYAML(setupOIDCTokenScript)
+ stepLines = append(stepLines, formattedScript...)
+
+ return GitHubActionStep(stepLines)
+}
+
+// GenerateOIDCRevokeStep generates a GitHub Actions step to revoke OIDC token
+func GenerateOIDCRevokeStep(oidcConfig *OIDCConfig) GitHubActionStep {
+ var stepLines []string
+
+ stepLines = append(stepLines, " - name: Revoke OIDC token")
+ stepLines = append(stepLines, " id: revoke_oidc_token")
+ stepLines = append(stepLines, " if: always() && steps.setup_oidc_token.outputs.token_source == 'oauth'")
+ stepLines = append(stepLines, " uses: actions/github-script@v8")
+ stepLines = append(stepLines, " env:")
+
+ // Add revoke URL if configured
+ if oidcConfig.TokenRevokeURL != "" {
+ stepLines = append(stepLines, fmt.Sprintf(" GH_AW_OIDC_REVOKE_URL: %s", oidcConfig.TokenRevokeURL))
+ }
+
+ // Pass token obtained status from setup step
+ stepLines = append(stepLines, " GH_AW_OIDC_TOKEN_OBTAINED: ${{ steps.setup_oidc_token.outputs.oidc_token_obtained }}")
+
+ // Pass the token for revocation
+ stepLines = append(stepLines, " GH_AW_OIDC_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}")
+
+ stepLines = append(stepLines, " with:")
+ stepLines = append(stepLines, " script: |")
+
+ // Add the JavaScript script with proper indentation
+ formattedScript := FormatJavaScriptForYAML(revokeOIDCTokenScript)
+ stepLines = append(stepLines, formattedScript...)
+
+ return GitHubActionStep(stepLines)
+}
diff --git a/pkg/workflow/openid_test.go b/pkg/workflow/openid_test.go
new file mode 100644
index 00000000000..e834b02693b
--- /dev/null
+++ b/pkg/workflow/openid_test.go
@@ -0,0 +1,216 @@
+package workflow
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestOIDCConfigExtraction(t *testing.T) {
+ compiler := &Compiler{
+ engineRegistry: GetGlobalEngineRegistry(),
+ }
+
+ // Test OIDC configuration extraction
+ frontmatter := map[string]any{
+ "engine": map[string]any{
+ "id": "claude",
+ "oidc": map[string]any{
+ "audience": "test-audience",
+ "token_exchange_url": "https://api.example.com/token-exchange",
+ "token_revoke_url": "https://api.example.com/token-revoke",
+ "oauth-token-env-var": "TEST_OAUTH_TOKEN",
+ "api-token-env-var": "TEST_API_TOKEN",
+ },
+ },
+ }
+
+ engineID, config := compiler.ExtractEngineConfig(frontmatter)
+
+ if engineID != "claude" {
+ t.Errorf("Expected engine ID 'claude', got '%s'", engineID)
+ }
+
+ if config == nil {
+ t.Fatal("Expected config to be non-nil")
+ }
+
+ if config.OIDC == nil {
+ t.Fatal("Expected OIDC config to be non-nil")
+ }
+
+ if config.OIDC.Audience != "test-audience" {
+ t.Errorf("Expected audience 'test-audience', got '%s'", config.OIDC.Audience)
+ }
+
+ if config.OIDC.TokenExchangeURL != "https://api.example.com/token-exchange" {
+ t.Errorf("Expected token exchange URL 'https://api.example.com/token-exchange', got '%s'", config.OIDC.TokenExchangeURL)
+ }
+
+ if config.OIDC.TokenRevokeURL != "https://api.example.com/token-revoke" {
+ t.Errorf("Expected token revoke URL 'https://api.example.com/token-revoke', got '%s'", config.OIDC.TokenRevokeURL)
+ }
+
+ if config.OIDC.OauthTokenEnvVar != "TEST_OAUTH_TOKEN" {
+ t.Errorf("Expected OAuth token env var 'TEST_OAUTH_TOKEN', got '%s'", config.OIDC.OauthTokenEnvVar)
+ }
+
+ if config.OIDC.ApiTokenEnvVar != "TEST_API_TOKEN" {
+ t.Errorf("Expected API token env var 'TEST_API_TOKEN', got '%s'", config.OIDC.ApiTokenEnvVar)
+ }
+}
+
+func TestOIDCConfigDefaults(t *testing.T) {
+ // Test with minimal OIDC configuration
+ oidcConfig := &OIDCConfig{
+ TokenExchangeURL: "https://api.example.com/exchange",
+ }
+
+ // Test default audience for Claude
+ audience := oidcConfig.GetOIDCAudience("claude")
+ if audience != "claude-code-github-action" {
+ t.Errorf("Expected default audience 'claude-code-github-action', got '%s'", audience)
+ }
+
+ // Test engine method for token env var name
+ claudeEngine := NewClaudeEngine()
+ envVarName := claudeEngine.GetTokenEnvVarName()
+ if envVarName != "ANTHROPIC_API_KEY" {
+ t.Errorf("Expected Claude env var name 'ANTHROPIC_API_KEY', got '%s'", envVarName)
+ }
+
+ // Test for other engines
+ copilotEngine := NewCopilotEngine()
+ copilotEnvVar := copilotEngine.GetTokenEnvVarName()
+ if copilotEnvVar != "GITHUB_TOKEN" {
+ t.Errorf("Expected Copilot env var name 'GITHUB_TOKEN', got '%s'", copilotEnvVar)
+ }
+
+ codexEngine := NewCodexEngine()
+ codexEnvVar := codexEngine.GetTokenEnvVarName()
+ if codexEnvVar != "OPENAI_API_KEY" {
+ t.Errorf("Expected Codex env var name 'OPENAI_API_KEY', got '%s'", codexEnvVar)
+ }
+}
+
+func TestClaudeEngineWithOIDC(t *testing.T) {
+ engine := NewClaudeEngine()
+ workflowData := &WorkflowData{
+ Name: "test-workflow",
+ MarkdownContent: "Test workflow",
+ EngineConfig: &EngineConfig{
+ ID: "claude",
+ OIDC: &OIDCConfig{
+ Audience: "claude-code-github-action",
+ TokenExchangeURL: "https://api.anthropic.com/api/github/github-app-token-exchange",
+ TokenRevokeURL: "https://api.anthropic.com/api/github/github-app-token-revoke",
+ },
+ },
+ Tools: map[string]any{},
+ }
+
+ steps := engine.GetExecutionSteps(workflowData, "/tmp/gh-aw/test.log")
+
+ // Convert steps to string for easier inspection
+ var stepsStr string
+ for _, step := range steps {
+ stepsStr += strings.Join(step, "\n") + "\n"
+ }
+
+ // Verify OIDC setup step is present
+ if !strings.Contains(stepsStr, "Setup OIDC token") {
+ t.Error("Expected OIDC setup step to be present")
+ }
+
+ // Verify OIDC revoke step is present
+ if !strings.Contains(stepsStr, "Revoke OIDC token") {
+ t.Error("Expected OIDC revoke step to be present")
+ }
+
+ // Verify OAuth token is used from OIDC setup step
+ if !strings.Contains(stepsStr, "CLAUDE_CODE_OAUTH_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}") {
+ t.Error("Expected CLAUDE_CODE_OAUTH_TOKEN to use token from OIDC setup step")
+ }
+
+ // Verify setup step uses github-script
+ if !strings.Contains(stepsStr, "uses: actions/github-script@v8") {
+ t.Error("Expected OIDC setup step to use actions/github-script@v8")
+ }
+
+ // Verify revoke step has refined always condition
+ if !strings.Contains(stepsStr, "if: always() && steps.setup_oidc_token.outputs.token != ''") {
+ t.Error("Expected OIDC revoke step to have 'if: always() && steps.setup_oidc_token.outputs.token != ''' condition")
+ }
+}
+
+func TestClaudeEngineWithoutOIDC(t *testing.T) {
+ engine := NewClaudeEngine()
+ workflowData := &WorkflowData{
+ Name: "test-workflow",
+ MarkdownContent: "Test workflow",
+ EngineConfig: &EngineConfig{
+ ID: "claude",
+ },
+ Tools: map[string]any{},
+ }
+
+ steps := engine.GetExecutionSteps(workflowData, "/tmp/gh-aw/test.log")
+
+ // Convert steps to string for easier inspection
+ var stepsStr string
+ for _, step := range steps {
+ stepsStr += strings.Join(step, "\n") + "\n"
+ }
+
+ // Verify OIDC setup step IS present (Claude has OIDC enabled by default)
+ if !strings.Contains(stepsStr, "Setup OIDC token") {
+ t.Error("Expected OIDC setup step to be present - Claude has OIDC enabled by default")
+ }
+
+ // Verify OIDC revoke step IS present (Claude has OIDC enabled by default)
+ if !strings.Contains(stepsStr, "Revoke OIDC token") {
+ t.Error("Expected OIDC revoke step to be present - Claude has OIDC enabled by default")
+ }
+
+ // Verify OAuth token uses OIDC token from setup step
+ if !strings.Contains(stepsStr, "CLAUDE_CODE_OAUTH_TOKEN: ${{ steps.setup_oidc_token.outputs.token }}") {
+ t.Error("Expected CLAUDE_CODE_OAUTH_TOKEN to use OAuth token from setup step - Claude has OIDC enabled by default")
+ }
+
+ // Verify default OIDC configuration values
+ if !strings.Contains(stepsStr, "GH_AW_OIDC_AUDIENCE: claude-code-github-action") {
+ t.Error("Expected default OIDC audience for Claude")
+ }
+
+ if !strings.Contains(stepsStr, "GH_AW_OIDC_EXCHANGE_URL: https://api.anthropic.com/api/github/github-app-token-exchange") {
+ t.Error("Expected default OIDC exchange URL for Claude")
+ }
+}
+
+func TestHasOIDCConfig(t *testing.T) {
+ // Test with nil config
+ if HasOIDCConfig(nil) {
+ t.Error("Expected HasOIDCConfig to return false for nil config")
+ }
+
+ // Test with config but no OIDC
+ config := &EngineConfig{
+ ID: "claude",
+ }
+ if HasOIDCConfig(config) {
+ t.Error("Expected HasOIDCConfig to return false when OIDC is nil")
+ }
+
+ // Test with OIDC but no token_exchange_url
+ config.OIDC = &OIDCConfig{
+ Audience: "test-audience",
+ }
+ if HasOIDCConfig(config) {
+ t.Error("Expected HasOIDCConfig to return false when token_exchange_url is not set")
+ }
+
+ // Test with OIDC and token_exchange_url
+ config.OIDC.TokenExchangeURL = "https://api.example.com/exchange"
+ if !HasOIDCConfig(config) {
+ t.Error("Expected HasOIDCConfig to return true when token_exchange_url is set")
+ }
+}