Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
568 changes: 566 additions & 2 deletions .github/workflows/audit-workflows.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/blog-auditor.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/changeset-generator.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/cli-version-checker.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/commit-changes-analyzer.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/copilot-agent-analysis.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/daily-doc-updater.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/example-workflow-analyzer.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/github-mcp-tools-report.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/go-logger.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/go-pattern-detector.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/instructions-janitor.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/lockfile-stats.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/schema-consistency-checker.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/scout.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/security-fix-pr.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/semantic-function-refactor.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/smoke-claude.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/smoke-detector.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/technical-doc-writer.lock.yml

Large diffs are not rendered by default.

568 changes: 566 additions & 2 deletions .github/workflows/unbloat-docs.lock.yml

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
44 changes: 44 additions & 0 deletions pkg/workflow/agentic_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
49 changes: 47 additions & 2 deletions pkg/workflow/claude_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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",
},
}
}

Expand Down Expand Up @@ -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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot move to base type agentic engine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved GetOIDCConfig logic to BaseEngine. Added GetOIDCConfigWithDefault helper method that engines can use with their default configurations. (c8e796c)

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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Move to Odicconfig type

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added GetOAuthTokenEnvVar method to OIDCConfig type. (c8e796c)

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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot love to odicconfig type

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added GetApiTokenEnvVar method to OIDCConfig type. (c8e796c)

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

Expand Down Expand Up @@ -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\"")
Expand Down Expand Up @@ -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
}

Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/codex_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/copilot_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions pkg/workflow/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions pkg/workflow/js.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
60 changes: 60 additions & 0 deletions pkg/workflow/js/revoke_oidc_token.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// @ts-check
/// <reference types="@actions/github-script" />

/**
* 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();
Loading
Loading