diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index ccdefb662c2..afe8d935dbd 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -179,6 +179,36 @@ engine: - **`max-turns`** (optional): Maximum number of chat iterations per run (cost-control option) - **`env`** (optional): Custom environment variables to pass to the agentic engine as key-value pairs +### Environment Variables and Secret Overrides + +The `env` option supports overriding default secrets and environment variables used by engines: + +**Basic Environment Variables:** +```yaml +engine: + id: claude + env: + AWS_REGION: us-west-2 + CUSTOM_API_ENDPOINT: https://api.example.com + DEBUG_MODE: "true" +``` + +**Secret Override Example:** + +You can override default secrets used by engines. This is particularly useful for Codex workflows when you need to use a different OpenAI API key: + +```yaml +engine: + id: codex + model: gpt-4 + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY_CI }} +``` + +This configuration overrides the default `OPENAI_API_KEY` secret with your custom secret, allowing you to use organization-specific API keys without duplicating secrets. + +### Turn Limiting + The `max-turns` option is configured within the engine configuration to limit the number of chat iterations within a single agentic run: ```yaml @@ -186,24 +216,13 @@ engine: id: claude max-turns: 5 ``` -0 + **Behavior:** 1. Passes the limit to the AI engine (e.g., Claude Code action) 2. Engine stops iterating when the turn limit is reached 3. Helps prevent runaway chat loops and control costs 4. Only applies to engines that support turn limiting (currently Claude) -The `env` option allows you to pass custom environment variables to the agentic engine: - -```yaml -engine: - id: claude - env: - - "AWS_REGION=us-west-2" - - "CUSTOM_API_ENDPOINT: https://api.example.com" - - "DEBUG_MODE: true" -``` - ## Tools Configuration (`tools:`) The `tools:` section specifies which tools and MCP (Model Context Protocol) servers are available to the AI engine. This enables integration with GitHub APIs, browser automation, and other external services. diff --git a/pkg/cli/workflows/test-claude-missing-tool.lock.yml b/pkg/cli/workflows/test-claude-missing-tool.lock.yml index bd13ae4ff4c..498c099d9a7 100644 --- a/pkg/cli/workflows/test-claude-missing-tool.lock.yml +++ b/pkg/cli/workflows/test-claude-missing-tool.lock.yml @@ -958,24 +958,21 @@ jobs: * @returns {string} The string with unknown domains redacted */ function sanitizeUrlDomains(s) { - return s.replace( - /\bhttps:\/\/[^\s\])}'"<>&\x00-\x1f,;]+/gi, - (match) => { - // Extract just the URL part after https:// - const urlAfterProtocol = match.slice(8); // Remove 'https://' - // Extract the hostname part (before first slash, colon, or other delimiter) - const hostname = urlAfterProtocol.split(/[\/:\?#]/)[0].toLowerCase(); - // Check if this domain or any parent domain is in the allowlist - const isAllowed = allowedDomains.some(allowedDomain => { - const normalizedAllowed = allowedDomain.toLowerCase(); - return ( - hostname === normalizedAllowed || - hostname.endsWith("." + normalizedAllowed) - ); - }); - return isAllowed ? match : "(redacted)"; - } - ); + return s.replace(/\bhttps:\/\/[^\s\])}'"<>&\x00-\x1f,;]+/gi, match => { + // Extract just the URL part after https:// + const urlAfterProtocol = match.slice(8); // Remove 'https://' + // Extract the hostname part (before first slash, colon, or other delimiter) + const hostname = urlAfterProtocol.split(/[\/:\?#]/)[0].toLowerCase(); + // Check if this domain or any parent domain is in the allowlist + const isAllowed = allowedDomains.some(allowedDomain => { + const normalizedAllowed = allowedDomain.toLowerCase(); + return ( + hostname === normalizedAllowed || + hostname.endsWith("." + normalizedAllowed) + ); + }); + return isAllowed ? match : "(redacted)"; + }); } /** * Remove unknown protocols except https @@ -1675,16 +1672,22 @@ jobs: } core.setOutput("output", JSON.stringify(validatedOutput)); core.setOutput("raw_output", outputContent); + // Write processed output to step summary using core.summary + try { + await core.summary + .addRaw("## Processed Output\n\n") + .addRaw("```json\n") + .addRaw(JSON.stringify(validatedOutput)) + .addRaw("\n```\n") + .write(); + core.info("Successfully wrote processed output to step summary"); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + core.warning(`Failed to write to step summary: ${errorMsg}`); + } } // Call the main function await main(); - - name: Print sanitized agent output - run: | - echo "## Processed Output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '``````json' >> $GITHUB_STEP_SUMMARY - echo '${{ steps.collect_output.outputs.output }}' >> $GITHUB_STEP_SUMMARY - echo '``````' >> $GITHUB_STEP_SUMMARY - name: Upload sanitized agent output if: always() && env.GITHUB_AW_AGENT_OUTPUT uses: actions/upload-artifact@v4 diff --git a/pkg/cli/workflows/test-codex-add-issue-comment.lock.yml b/pkg/cli/workflows/test-codex-add-issue-comment.lock.yml index f91e469f5d4..2fd3351bd55 100644 --- a/pkg/cli/workflows/test-codex-add-issue-comment.lock.yml +++ b/pkg/cli/workflows/test-codex-add-issue-comment.lock.yml @@ -132,7 +132,7 @@ jobs: codex --version # Authenticate with Codex - codex login --api-key "${{ secrets.OPENAI_API_KEY }}" + codex login --api-key "$OPENAI_API_KEY" # Run codex with log capture - pipefail ensures codex exit code is preserved codex exec --full-auto "$INSTRUCTION" 2>&1 | tee /tmp/test-codex-add-issue-comment.log diff --git a/pkg/cli/workflows/test-codex-add-issue-labels.lock.yml b/pkg/cli/workflows/test-codex-add-issue-labels.lock.yml index 93b0da123e5..ae49eebb6f1 100644 --- a/pkg/cli/workflows/test-codex-add-issue-labels.lock.yml +++ b/pkg/cli/workflows/test-codex-add-issue-labels.lock.yml @@ -132,7 +132,7 @@ jobs: codex --version # Authenticate with Codex - codex login --api-key "${{ secrets.OPENAI_API_KEY }}" + codex login --api-key "$OPENAI_API_KEY" # Run codex with log capture - pipefail ensures codex exit code is preserved codex exec --full-auto "$INSTRUCTION" 2>&1 | tee /tmp/test-codex-add-issue-labels.log diff --git a/pkg/cli/workflows/test-codex-command.lock.yml b/pkg/cli/workflows/test-codex-command.lock.yml index e93cce1abb6..329bf9e8c10 100644 --- a/pkg/cli/workflows/test-codex-command.lock.yml +++ b/pkg/cli/workflows/test-codex-command.lock.yml @@ -132,7 +132,7 @@ jobs: codex --version # Authenticate with Codex - codex login --api-key "${{ secrets.OPENAI_API_KEY }}" + codex login --api-key "$OPENAI_API_KEY" # Run codex with log capture - pipefail ensures codex exit code is preserved codex exec --full-auto "$INSTRUCTION" 2>&1 | tee /tmp/test-codex-command.log diff --git a/pkg/cli/workflows/test-codex-custom-env.lock.yml b/pkg/cli/workflows/test-codex-custom-env.lock.yml new file mode 100644 index 00000000000..607c723901f --- /dev/null +++ b/pkg/cli/workflows/test-codex-custom-env.lock.yml @@ -0,0 +1,577 @@ +# This file was automatically generated by gh-aw. DO NOT EDIT. +# To update this file, edit the corresponding .md file and run: +# gh aw compile + +name: "Test Codex Custom Environment Variable" +on: + workflow_dispatch: null + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Test Codex Custom Environment Variable" + +jobs: + test-codex-custom-environment-variable: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v5 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + - name: Install Codex + run: npm install -g @openai/codex + - name: Setup MCPs + run: | + mkdir -p /tmp/mcp-config + cat > /tmp/mcp-config/config.toml << EOF + [history] + persistence = "none" + + [mcp_servers.github] + user_agent = "test-codex-custom-environment-variable" + command = "docker" + args = [ + "run", + "-i", + "--rm", + "-e", + "GITHUB_PERSONAL_ACCESS_TOKEN", + "ghcr.io/github/github-mcp-server:sha-09deac4" + ] + env = { "GITHUB_PERSONAL_ACCESS_TOKEN" = "${{ secrets.GITHUB_TOKEN }}" } + EOF + - name: Create prompt + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + run: | + mkdir -p /tmp/aw-prompts + cat > $GITHUB_AW_PROMPT << 'EOF' + # Test Codex Custom Environment Variable + + This is a test workflow to demonstrate how to configure custom environment variables for the Codex engine, specifically overriding the default OPENAI_API_KEY secret. + + Please analyze the current repository structure and list the main directories and their purposes. + + EOF + - name: Print prompt to step summary + run: | + echo "## Generated Prompt" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '``````markdown' >> $GITHUB_STEP_SUMMARY + cat $GITHUB_AW_PROMPT >> $GITHUB_STEP_SUMMARY + echo '``````' >> $GITHUB_STEP_SUMMARY + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + - name: Generate agentic run info + uses: actions/github-script@v8 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "codex", + engine_name: "Codex", + model: "", + version: "", + workflow_name: "Test Codex Custom Environment Variable", + experimental: true, + supports_tools_whitelist: true, + supports_http_transport: false, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + staged: false, + created_at: new Date().toISOString() + }; + + // Write to /tmp directory to avoid inclusion in PR + const tmpPath = '/tmp/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + + // Add agentic workflow run information to step summary + core.summary + .addRaw('## Agentic Run Information\n\n') + .addRaw('```json\n') + .addRaw(JSON.stringify(awInfo, null, 2)) + .addRaw('\n```\n') + .write(); + - name: Upload agentic run info + if: always() + uses: actions/upload-artifact@v4 + with: + name: aw_info.json + path: /tmp/aw_info.json + if-no-files-found: warn + - name: Run Codex + run: | + set -o pipefail + INSTRUCTION=$(cat /tmp/aw-prompts/prompt.txt) + export CODEX_HOME=/tmp/mcp-config + + # Create log directory outside git repo + mkdir -p /tmp/aw-logs + + # where is Codex + which codex + + # Check Codex version + codex --version + + # Authenticate with Codex + codex login --api-key "$OPENAI_API_KEY" + + # Run codex with log capture - pipefail ensures codex exit code is preserved + codex exec --full-auto "$INSTRUCTION" 2>&1 | tee /tmp/test-codex-custom-environment-variable.log + env: + GITHUB_AW_PROMPT: /tmp/aw-prompts/prompt.txt + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY_CI }} + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@v8 + env: + GITHUB_AW_AGENT_OUTPUT: /tmp/test-codex-custom-environment-variable.log + with: + script: | + function main() { + const fs = require("fs"); + try { + const logFile = process.env.GITHUB_AW_AGENT_OUTPUT; + if (!logFile) { + core.info("No agent log file specified"); + return; + } + if (!fs.existsSync(logFile)) { + core.info(`Log file not found: ${logFile}`); + return; + } + const content = fs.readFileSync(logFile, "utf8"); + const parsedLog = parseCodexLog(content); + if (parsedLog) { + core.summary.addRaw(parsedLog).write(); + core.info("Codex log parsed successfully"); + } else { + core.error("Failed to parse Codex log"); + } + } catch (error) { + core.setFailed(error instanceof Error ? error : String(error)); + } + } + /** + * Parse codex log content and format as markdown + * @param {string} logContent - The raw log content to parse + * @returns {string} Formatted markdown content + */ + function parseCodexLog(logContent) { + try { + const lines = logContent.split("\n"); + let markdown = "## 🤖 Commands and Tools\n\n"; + const commandSummary = []; + // First pass: collect commands for summary + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + // Detect tool usage and exec commands + if (line.includes("] tool ") && line.includes("(")) { + // Extract tool name + const toolMatch = line.match(/\] tool ([^(]+)\(/); + if (toolMatch) { + const toolName = toolMatch[1]; + // Look ahead to find the result status + let statusIcon = "❓"; // Unknown by default + for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) { + const nextLine = lines[j]; + if (nextLine.includes("success in")) { + statusIcon = "✅"; + break; + } else if ( + nextLine.includes("failure in") || + nextLine.includes("error in") || + nextLine.includes("failed in") + ) { + statusIcon = "❌"; + break; + } + } + if (toolName.includes(".")) { + // Format as provider::method + const parts = toolName.split("."); + const provider = parts[0]; + const method = parts.slice(1).join("_"); + commandSummary.push( + `* ${statusIcon} \`${provider}::${method}(...)\`` + ); + } else { + commandSummary.push(`* ${statusIcon} \`${toolName}(...)\``); + } + } + } else if (line.includes("] exec ")) { + // Extract exec command + const execMatch = line.match(/exec (.+?) in/); + if (execMatch) { + const formattedCommand = formatBashCommand(execMatch[1]); + // Look ahead to find the result status + let statusIcon = "❓"; // Unknown by default + for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) { + const nextLine = lines[j]; + if (nextLine.includes("succeeded in")) { + statusIcon = "✅"; + break; + } else if ( + nextLine.includes("failed in") || + nextLine.includes("error") + ) { + statusIcon = "❌"; + break; + } + } + commandSummary.push(`* ${statusIcon} \`${formattedCommand}\``); + } + } + } + // Add command summary + if (commandSummary.length > 0) { + for (const cmd of commandSummary) { + markdown += `${cmd}\n`; + } + } else { + markdown += "No commands or tools used.\n"; + } + // Add Information section + markdown += "\n## 📊 Information\n\n"; + // Extract metadata from Codex logs + let totalTokens = 0; + const tokenMatches = logContent.match(/tokens used: (\d+)/g); + if (tokenMatches) { + for (const match of tokenMatches) { + const numberMatch = match.match(/(\d+)/); + if (numberMatch) { + const tokens = parseInt(numberMatch[1]); + totalTokens += tokens; + } + } + } + if (totalTokens > 0) { + markdown += `**Total Tokens Used:** ${totalTokens.toLocaleString()}\n\n`; + } + // Count tool calls and exec commands + const toolCalls = (logContent.match(/\] tool /g) || []).length; + const execCommands = (logContent.match(/\] exec /g) || []).length; + if (toolCalls > 0) { + markdown += `**Tool Calls:** ${toolCalls}\n\n`; + } + if (execCommands > 0) { + markdown += `**Commands Executed:** ${execCommands}\n\n`; + } + markdown += "\n## 🤖 Reasoning\n\n"; + // Second pass: process full conversation flow with interleaved reasoning, tools, and commands + let inThinkingSection = false; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + // Skip metadata lines + if ( + line.includes("OpenAI Codex") || + line.startsWith("--------") || + line.includes("workdir:") || + line.includes("model:") || + line.includes("provider:") || + line.includes("approval:") || + line.includes("sandbox:") || + line.includes("reasoning effort:") || + line.includes("reasoning summaries:") || + line.includes("tokens used:") + ) { + continue; + } + // Process thinking sections + if (line.includes("] thinking")) { + inThinkingSection = true; + continue; + } + // Process tool calls + if (line.includes("] tool ") && line.includes("(")) { + inThinkingSection = false; + const toolMatch = line.match(/\] tool ([^(]+)\(/); + if (toolMatch) { + const toolName = toolMatch[1]; + // Look ahead to find the result status + let statusIcon = "❓"; // Unknown by default + for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) { + const nextLine = lines[j]; + if (nextLine.includes("success in")) { + statusIcon = "✅"; + break; + } else if ( + nextLine.includes("failure in") || + nextLine.includes("error in") || + nextLine.includes("failed in") + ) { + statusIcon = "❌"; + break; + } + } + if (toolName.includes(".")) { + const parts = toolName.split("."); + const provider = parts[0]; + const method = parts.slice(1).join("_"); + markdown += `${statusIcon} ${provider}::${method}(...)\n\n`; + } else { + markdown += `${statusIcon} ${toolName}(...)\n\n`; + } + } + continue; + } + // Process exec commands + if (line.includes("] exec ")) { + inThinkingSection = false; + const execMatch = line.match(/exec (.+?) in/); + if (execMatch) { + const formattedCommand = formatBashCommand(execMatch[1]); + // Look ahead to find the result status + let statusIcon = "❓"; // Unknown by default + for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) { + const nextLine = lines[j]; + if (nextLine.includes("succeeded in")) { + statusIcon = "✅"; + break; + } else if ( + nextLine.includes("failed in") || + nextLine.includes("error") + ) { + statusIcon = "❌"; + break; + } + } + markdown += `${statusIcon} \`${formattedCommand}\`\n\n`; + } + continue; + } + // Process thinking content + if ( + inThinkingSection && + line.trim().length > 20 && + !line.startsWith("[2025-") + ) { + const trimmed = line.trim(); + // Add thinking content directly + markdown += `${trimmed}\n\n`; + } + } + return markdown; + } catch (error) { + core.error(`Error parsing Codex log: ${error}`); + return "## 🤖 Commands and Tools\n\nError parsing log content.\n\n## 🤖 Reasoning\n\nUnable to parse reasoning from log.\n\n"; + } + } + /** + * Format bash command for display + * @param {string} command - The command to format + * @returns {string} Formatted command string + */ + function formatBashCommand(command) { + if (!command) return ""; + // Convert multi-line commands to single line by replacing newlines with spaces + // and collapsing multiple spaces + let formatted = command + .replace(/\n/g, " ") // Replace newlines with spaces + .replace(/\r/g, " ") // Replace carriage returns with spaces + .replace(/\t/g, " ") // Replace tabs with spaces + .replace(/\s+/g, " ") // Collapse multiple spaces into one + .trim(); // Remove leading/trailing whitespace + // Escape backticks to prevent markdown issues + formatted = formatted.replace(/`/g, "\\`"); + // Truncate if too long (keep reasonable length for summary) + const maxLength = 80; + if (formatted.length > maxLength) { + formatted = formatted.substring(0, maxLength) + "..."; + } + return formatted; + } + /** + * Truncate string to maximum length + * @param {string} str - The string to truncate + * @param {number} maxLength - Maximum length allowed + * @returns {string} Truncated string + */ + function truncateString(str, maxLength) { + if (!str) return ""; + if (str.length <= maxLength) return str; + return str.substring(0, maxLength) + "..."; + } + // Export for testing + if (typeof module !== "undefined" && module.exports) { + module.exports = { parseCodexLog, formatBashCommand, truncateString }; + } + main(); + - name: Upload agent logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-codex-custom-environment-variable.log + path: /tmp/test-codex-custom-environment-variable.log + if-no-files-found: warn + - name: Validate agent logs for errors + if: always() + uses: actions/github-script@v8 + env: + GITHUB_AW_AGENT_OUTPUT: /tmp/test-codex-custom-environment-variable.log + GITHUB_AW_ERROR_PATTERNS: "[{\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2})\\\\]\\\\s+stream\\\\s+(error):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Codex stream errors with timestamp\"},{\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2})\\\\]\\\\s+(ERROR):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Codex ERROR messages with timestamp\"},{\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2})\\\\]\\\\s+(WARN|WARNING):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Codex warning messages with timestamp\"}]" + with: + script: | + function main() { + const fs = require("fs"); + try { + const logFile = process.env.GITHUB_AW_AGENT_OUTPUT; + if (!logFile) { + throw new Error( + "GITHUB_AW_AGENT_OUTPUT environment variable is required" + ); + } + if (!fs.existsSync(logFile)) { + throw new Error(`Log file not found: ${logFile}`); + } + // Get error patterns from environment variables + const patterns = getErrorPatternsFromEnv(); + if (patterns.length === 0) { + throw new Error( + "GITHUB_AW_ERROR_PATTERNS environment variable is required and must contain at least one pattern" + ); + } + const content = fs.readFileSync(logFile, "utf8"); + const hasErrors = validateErrors(content, patterns); + if (hasErrors) { + core.setFailed("Errors detected in agent logs - failing workflow step"); + } else { + core.info("Error validation completed successfully"); + } + } catch (error) { + console.debug(error); + core.setFailed( + `Error validating log: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + function getErrorPatternsFromEnv() { + const patternsEnv = process.env.GITHUB_AW_ERROR_PATTERNS; + if (!patternsEnv) { + throw new Error( + "GITHUB_AW_ERROR_PATTERNS environment variable is required" + ); + } + try { + const patterns = JSON.parse(patternsEnv); + if (!Array.isArray(patterns)) { + throw new Error("GITHUB_AW_ERROR_PATTERNS must be a JSON array"); + } + return patterns; + } catch (e) { + throw new Error( + `Failed to parse GITHUB_AW_ERROR_PATTERNS as JSON: ${e instanceof Error ? e.message : String(e)}` + ); + } + } + /** + * @param {string} logContent + * @param {any[]} patterns + * @returns {boolean} + */ + function validateErrors(logContent, patterns) { + const lines = logContent.split("\n"); + let hasErrors = false; + for (const pattern of patterns) { + const regex = new RegExp(pattern.pattern, "g"); + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + const line = lines[lineIndex]; + let match; + while ((match = regex.exec(line)) !== null) { + const level = extractLevel(match, pattern); + const message = extractMessage(match, pattern, line); + const errorMessage = `Line ${lineIndex + 1}: ${message} (Pattern: ${pattern.description || "Unknown pattern"}, Raw log: ${truncateString(line.trim(), 120)})`; + if (level.toLowerCase() === "error") { + core.error(errorMessage); + hasErrors = true; + } else { + core.warning(errorMessage); + } + } + } + } + return hasErrors; + } + /** + * @param {any} match + * @param {any} pattern + * @returns {string} + */ + function extractLevel(match, pattern) { + if ( + pattern.level_group && + pattern.level_group > 0 && + match[pattern.level_group] + ) { + return match[pattern.level_group]; + } + // Try to infer level from the match content + const fullMatch = match[0]; + if (fullMatch.toLowerCase().includes("error")) { + return "error"; + } else if (fullMatch.toLowerCase().includes("warn")) { + return "warning"; + } + return "unknown"; + } + /** + * @param {any} match + * @param {any} pattern + * @param {any} fullLine + * @returns {string} + */ + function extractMessage(match, pattern, fullLine) { + if ( + pattern.message_group && + pattern.message_group > 0 && + match[pattern.message_group] + ) { + return match[pattern.message_group].trim(); + } + // Fallback to the full match or line + return match[0] || fullLine.trim(); + } + /** + * @param {any} str + * @param {any} maxLength + * @returns {string} + */ + function truncateString(str, maxLength) { + if (!str) return ""; + if (str.length <= maxLength) return str; + return str.substring(0, maxLength) + "..."; + } + // Export for testing + if (typeof module !== "undefined" && module.exports) { + module.exports = { + validateErrors, + extractLevel, + extractMessage, + getErrorPatternsFromEnv, + truncateString, + }; + } + // Only run main if this script is executed directly, not when imported for testing + if (typeof module === "undefined" || require.main === module) { + main(); + } + diff --git a/pkg/cli/workflows/test-codex-custom-env.md b/pkg/cli/workflows/test-codex-custom-env.md new file mode 100644 index 00000000000..75776cdd2ca --- /dev/null +++ b/pkg/cli/workflows/test-codex-custom-env.md @@ -0,0 +1,16 @@ +--- +on: + workflow_dispatch: +permissions: + contents: read +engine: + id: codex + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY_CI }} +--- + +# Test Codex Custom Environment Variable + +This is a test workflow to demonstrate how to configure custom environment variables for the Codex engine, specifically overriding the default OPENAI_API_KEY secret. + +Please analyze the current repository structure and list the main directories and their purposes. \ No newline at end of file diff --git a/pkg/cli/workflows/test-playwright-accessibility-contrast.lock.yml b/pkg/cli/workflows/test-playwright-accessibility-contrast.lock.yml index 7d5250ce08a..1e8a7e3411f 100644 --- a/pkg/cli/workflows/test-playwright-accessibility-contrast.lock.yml +++ b/pkg/cli/workflows/test-playwright-accessibility-contrast.lock.yml @@ -922,24 +922,21 @@ jobs: * @returns {string} The string with unknown domains redacted */ function sanitizeUrlDomains(s) { - return s.replace( - /\bhttps:\/\/[^\s\])}'"<>&\x00-\x1f,;]+/gi, - (match) => { - // Extract just the URL part after https:// - const urlAfterProtocol = match.slice(8); // Remove 'https://' - // Extract the hostname part (before first slash, colon, or other delimiter) - const hostname = urlAfterProtocol.split(/[\/:\?#]/)[0].toLowerCase(); - // Check if this domain or any parent domain is in the allowlist - const isAllowed = allowedDomains.some(allowedDomain => { - const normalizedAllowed = allowedDomain.toLowerCase(); - return ( - hostname === normalizedAllowed || - hostname.endsWith("." + normalizedAllowed) - ); - }); - return isAllowed ? match : "(redacted)"; - } - ); + return s.replace(/\bhttps:\/\/[^\s\])}'"<>&\x00-\x1f,;]+/gi, match => { + // Extract just the URL part after https:// + const urlAfterProtocol = match.slice(8); // Remove 'https://' + // Extract the hostname part (before first slash, colon, or other delimiter) + const hostname = urlAfterProtocol.split(/[\/:\?#]/)[0].toLowerCase(); + // Check if this domain or any parent domain is in the allowlist + const isAllowed = allowedDomains.some(allowedDomain => { + const normalizedAllowed = allowedDomain.toLowerCase(); + return ( + hostname === normalizedAllowed || + hostname.endsWith("." + normalizedAllowed) + ); + }); + return isAllowed ? match : "(redacted)"; + }); } /** * Remove unknown protocols except https @@ -1639,16 +1636,22 @@ jobs: } core.setOutput("output", JSON.stringify(validatedOutput)); core.setOutput("raw_output", outputContent); + // Write processed output to step summary using core.summary + try { + await core.summary + .addRaw("## Processed Output\n\n") + .addRaw("```json\n") + .addRaw(JSON.stringify(validatedOutput)) + .addRaw("\n```\n") + .write(); + core.info("Successfully wrote processed output to step summary"); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + core.warning(`Failed to write to step summary: ${errorMsg}`); + } } // Call the main function await main(); - - name: Print sanitized agent output - run: | - echo "## Processed Output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '``````json' >> $GITHUB_STEP_SUMMARY - echo '${{ steps.collect_output.outputs.output }}' >> $GITHUB_STEP_SUMMARY - echo '``````' >> $GITHUB_STEP_SUMMARY - name: Upload sanitized agent output if: always() && env.GITHUB_AW_AGENT_OUTPUT uses: actions/upload-artifact@v4 diff --git a/pkg/workflow/codex_engine.go b/pkg/workflow/codex_engine.go index e6d22af8d0d..bbf9094712f 100644 --- a/pkg/workflow/codex_engine.go +++ b/pkg/workflow/codex_engine.go @@ -114,7 +114,7 @@ which codex codex --version # Authenticate with Codex -codex login --api-key "${{ secrets.OPENAI_API_KEY }}" +codex login --api-key "$OPENAI_API_KEY" # Run codex with log capture - pipefail ensures codex exit code is preserved codex exec %s%s--full-auto "$INSTRUCTION" 2>&1 | tee %s`, modelParam, searchParam, logFile)