diff --git a/.github/workflows/example-engine-network-permissions.lock.yml b/.github/workflows/example-engine-network-permissions.lock.yml index 2a422bbba3a..49a28db3ce5 100644 --- a/.github/workflows/example-engine-network-permissions.lock.yml +++ b/.github/workflows/example-engine-network-permissions.lock.yml @@ -22,8 +22,6 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - outputs: - output: ${{ steps.collect_output.outputs.output }} steps: - name: Checkout repository uses: actions/checkout@v5 @@ -134,31 +132,6 @@ jobs: } } EOF - - name: Setup agent output - id: setup_agent_output - uses: actions/github-script@v7 - with: - script: | - function main() { - const fs = require('fs'); - const crypto = require('crypto'); - // Generate a random filename for the output file - const randomId = crypto.randomBytes(8).toString('hex'); - const outputFile = `/tmp/aw_output_${randomId}.txt`; - // Ensure the /tmp directory exists and create empty output file - fs.mkdirSync('/tmp', { recursive: true }); - fs.writeFileSync(outputFile, '', { mode: 0o644 }); - // Verify the file was created and is writable - if (!fs.existsSync(outputFile)) { - throw new Error(`Failed to create output file: ${outputFile}`); - } - // Set the environment variable for subsequent steps - core.exportVariable('GITHUB_AW_OUTPUT', outputFile); - console.log('Created agentic output file:', outputFile); - // Also set as step output for reference - core.setOutput('output_file', outputFile); - } - main(); - name: Setup MCPs run: | mkdir -p /tmp/mcp-config @@ -183,8 +156,6 @@ jobs: } EOF - name: Create prompt - env: - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} run: | mkdir -p /tmp/aw-prompts cat > /tmp/aw-prompts/prompt.txt << 'EOF' @@ -192,10 +163,6 @@ jobs: Please research the GitHub API documentation or Stack Overflow and find information about repository topics. Summarize them in a brief report. - - --- - - **IMPORTANT**: If you need to provide output that should be captured as a workflow output variable, write it to the file "${{ env.GITHUB_AW_OUTPUT }}". This file is available for you to write any output that should be exposed from this workflow. The content of this file will be made available as the 'output' workflow output. EOF - name: Print prompt to step summary run: | @@ -303,13 +270,10 @@ jobs: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} claude_env: | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} mcp_config: /tmp/mcp-config/mcp-servers.json prompt_file: /tmp/aw-prompts/prompt.txt settings: .claude/settings.json timeout_minutes: 5 - env: - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} - name: Capture Agentic Action logs if: always() run: | @@ -338,160 +302,6 @@ jobs: with: name: workflow-complete path: workflow-complete.txt - - name: Collect agent output - id: collect_output - uses: actions/github-script@v7 - with: - script: | - /** - * Sanitizes content for safe output in GitHub Actions - * @param {string} content - The content to sanitize - * @returns {string} The sanitized content - */ - function sanitizeContent(content) { - if (!content || typeof content !== 'string') { - return ''; - } - // Read allowed domains from environment variable - const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS; - const defaultAllowedDomains = [ - 'github.com', - 'github.io', - 'githubusercontent.com', - 'githubassets.com', - 'github.dev', - 'codespaces.new' - ]; - const allowedDomains = allowedDomainsEnv - ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d) - : defaultAllowedDomains; - let sanitized = content; - // Neutralize @mentions to prevent unintended notifications - sanitized = neutralizeMentions(sanitized); - // Remove control characters (except newlines and tabs) - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); - // XML character escaping - sanitized = sanitized - .replace(/&/g, '&') // Must be first to avoid double-escaping - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - // URI filtering - replace non-https protocols with "(redacted)" - // Step 1: Temporarily mark HTTPS URLs to protect them - sanitized = sanitizeUrlProtocols(sanitized); - // Domain filtering for HTTPS URIs - // Match https:// URIs and check if domain is in allowlist - sanitized = sanitizeUrlDomains(sanitized); - // Limit total length to prevent DoS (0.5MB max) - const maxLength = 524288; - if (sanitized.length > maxLength) { - sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]'; - } - // Limit number of lines to prevent log flooding (65k max) - const lines = sanitized.split('\n'); - const maxLines = 65000; - if (lines.length > maxLines) { - sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]'; - } - // Remove ANSI escape sequences - sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ''); - // Neutralize common bot trigger phrases - sanitized = neutralizeBotTriggers(sanitized); - // Trim excessive whitespace - return sanitized.trim(); - /** - * Remove unknown domains - * @param {string} s - The string to process - * @returns {string} The string with unknown domains redacted - */ - function sanitizeUrlDomains(s) { - s = s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => { - // Extract the hostname part (before first slash, colon, or other delimiter) - const hostname = domain.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; - } - /** - * Remove unknown protocols except https - * @param {string} s - The string to process - * @returns {string} The string with non-https protocols redacted - */ - function sanitizeUrlProtocols(s) { - // Match both protocol:// and protocol: patterns - // This covers URLs like https://example.com, javascript:alert(), mailto:user@domain.com, etc. - return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => { - // Allow https (case insensitive), redact everything else - return protocol.toLowerCase() === 'https' ? match : '(redacted)'; - }); - } - /** - * Neutralizes @mentions by wrapping them in backticks - * @param {string} s - The string to process - * @returns {string} The string with neutralized mentions - */ - function neutralizeMentions(s) { - // Replace @name or @org/team outside code with `@name` - return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, - (_m, p1, p2) => `${p1}\`@${p2}\``); - } - /** - * Neutralizes bot trigger phrases by wrapping them in backticks - * @param {string} s - The string to process - * @returns {string} The string with neutralized bot triggers - */ - function neutralizeBotTriggers(s) { - // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc. - return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi, - (match, action, ref) => `\`${action} #${ref}\``); - } - } - async function main() { - const fs = require("fs"); - const outputFile = process.env.GITHUB_AW_OUTPUT; - if (!outputFile) { - console.log('GITHUB_AW_OUTPUT not set, no output to collect'); - core.setOutput('output', ''); - return; - } - if (!fs.existsSync(outputFile)) { - console.log('Output file does not exist:', outputFile); - core.setOutput('output', ''); - return; - } - const outputContent = fs.readFileSync(outputFile, 'utf8'); - if (outputContent.trim() === '') { - console.log('Output file is empty'); - core.setOutput('output', ''); - } else { - const sanitizedContent = sanitizeContent(outputContent); - console.log('Collected agentic output (sanitized):', sanitizedContent.substring(0, 200) + (sanitizedContent.length > 200 ? '...' : '')); - core.setOutput('output', sanitizedContent); - } - } - await main(); - - name: Print agent output to step summary - env: - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} - run: | - echo "## Agent Output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '``````markdown' >> $GITHUB_STEP_SUMMARY - cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY - echo '``````' >> $GITHUB_STEP_SUMMARY - - name: Upload agentic output file - if: always() && steps.collect_output.outputs.output != '' - uses: actions/upload-artifact@v4 - with: - name: aw_output.txt - path: ${{ env.GITHUB_AW_OUTPUT }} - if-no-files-found: warn - name: Upload engine output files if: always() uses: actions/upload-artifact@v4 @@ -507,66 +317,4 @@ jobs: name: secure-web-research-task.log path: /tmp/secure-web-research-task.log if-no-files-found: warn - - name: Generate git patch - if: always() - run: | - # Check current git status - echo "Current git status:" - git status - # Get the initial commit SHA from the base branch of the pull request - if [ "$GITHUB_EVENT_NAME" = "pull_request" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ]; then - INITIAL_SHA="$GITHUB_BASE_REF" - else - INITIAL_SHA="$GITHUB_SHA" - fi - echo "Base commit SHA: $INITIAL_SHA" - # Configure git user for GitHub Actions - git config --global user.email "action@github.com" - git config --global user.name "GitHub Action" - # Stage any unstaged files - git add -A || true - # Check if there are staged files to commit - if ! git diff --cached --quiet; then - echo "Staged files found, committing them..." - git commit -m "[agent] staged files" || true - echo "Staged files committed" - else - echo "No staged files to commit" - fi - # Check updated git status - echo "Updated git status after committing staged files:" - git status - # Show compact diff information between initial commit and HEAD (committed changes only) - echo '## Git diff' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - git diff --name-only "$INITIAL_SHA"..HEAD >> $GITHUB_STEP_SUMMARY || true - echo '```' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - # Check if there are any committed changes since the initial commit - if git diff --quiet "$INITIAL_SHA" HEAD; then - echo "No committed changes detected since initial commit" - echo "Skipping patch generation - no committed changes to create patch from" - else - echo "Committed changes detected, generating patch..." - # Generate patch from initial commit to HEAD (committed changes only) - git format-patch "$INITIAL_SHA"..HEAD --stdout > /tmp/aw.patch || echo "Failed to generate patch" > /tmp/aw.patch - echo "Patch file created at /tmp/aw.patch" - ls -la /tmp/aw.patch - # Show the first 50 lines of the patch for review - echo '## Git Patch' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - echo '```diff' >> $GITHUB_STEP_SUMMARY - head -50 /tmp/aw.patch >> $GITHUB_STEP_SUMMARY || echo "Could not display patch contents" >> $GITHUB_STEP_SUMMARY - echo '...' >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - fi - - name: Upload git patch - if: always() - uses: actions/upload-artifact@v4 - with: - name: aw.patch - path: /tmp/aw.patch - if-no-files-found: ignore diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index da9c8126cfb..6c20465ee26 100644 --- a/.github/workflows/issue-triage.lock.yml +++ b/.github/workflows/issue-triage.lock.yml @@ -28,36 +28,9 @@ jobs: models: read pull-requests: read statuses: read - outputs: - output: ${{ steps.collect_output.outputs.output }} steps: - name: Checkout repository uses: actions/checkout@v5 - - name: Setup agent output - id: setup_agent_output - uses: actions/github-script@v7 - with: - script: | - function main() { - const fs = require('fs'); - const crypto = require('crypto'); - // Generate a random filename for the output file - const randomId = crypto.randomBytes(8).toString('hex'); - const outputFile = `/tmp/aw_output_${randomId}.txt`; - // Ensure the /tmp directory exists and create empty output file - fs.mkdirSync('/tmp', { recursive: true }); - fs.writeFileSync(outputFile, '', { mode: 0o644 }); - // Verify the file was created and is writable - if (!fs.existsSync(outputFile)) { - throw new Error(`Failed to create output file: ${outputFile}`); - } - // Set the environment variable for subsequent steps - core.exportVariable('GITHUB_AW_OUTPUT', outputFile); - console.log('Created agentic output file:', outputFile); - // Also set as step output for reference - core.setOutput('output_file', outputFile); - } - main(); - name: Setup MCPs run: | mkdir -p /tmp/mcp-config @@ -82,8 +55,6 @@ jobs: } EOF - name: Create prompt - env: - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} run: | mkdir -p /tmp/aw-prompts cat > /tmp/aw-prompts/prompt.txt << 'EOF' @@ -198,10 +169,6 @@ jobs: - List labels: `gh label list ...` - View label: `gh label view ...` - - --- - - **IMPORTANT**: If you need to provide output that should be captured as a workflow output variable, write it to the file "${{ env.GITHUB_AW_OUTPUT }}". This file is available for you to write any output that should be exposed from this workflow. The content of this file will be made available as the 'output' workflow output. EOF - name: Print prompt to step summary run: | @@ -317,12 +284,9 @@ jobs: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} claude_env: | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} mcp_config: /tmp/mcp-config/mcp-servers.json prompt_file: /tmp/aw-prompts/prompt.txt timeout_minutes: 10 - env: - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} - name: Capture Agentic Action logs if: always() run: | @@ -351,160 +315,6 @@ jobs: with: name: workflow-complete path: workflow-complete.txt - - name: Collect agent output - id: collect_output - uses: actions/github-script@v7 - with: - script: | - /** - * Sanitizes content for safe output in GitHub Actions - * @param {string} content - The content to sanitize - * @returns {string} The sanitized content - */ - function sanitizeContent(content) { - if (!content || typeof content !== 'string') { - return ''; - } - // Read allowed domains from environment variable - const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS; - const defaultAllowedDomains = [ - 'github.com', - 'github.io', - 'githubusercontent.com', - 'githubassets.com', - 'github.dev', - 'codespaces.new' - ]; - const allowedDomains = allowedDomainsEnv - ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d) - : defaultAllowedDomains; - let sanitized = content; - // Neutralize @mentions to prevent unintended notifications - sanitized = neutralizeMentions(sanitized); - // Remove control characters (except newlines and tabs) - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); - // XML character escaping - sanitized = sanitized - .replace(/&/g, '&') // Must be first to avoid double-escaping - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - // URI filtering - replace non-https protocols with "(redacted)" - // Step 1: Temporarily mark HTTPS URLs to protect them - sanitized = sanitizeUrlProtocols(sanitized); - // Domain filtering for HTTPS URIs - // Match https:// URIs and check if domain is in allowlist - sanitized = sanitizeUrlDomains(sanitized); - // Limit total length to prevent DoS (0.5MB max) - const maxLength = 524288; - if (sanitized.length > maxLength) { - sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]'; - } - // Limit number of lines to prevent log flooding (65k max) - const lines = sanitized.split('\n'); - const maxLines = 65000; - if (lines.length > maxLines) { - sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]'; - } - // Remove ANSI escape sequences - sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ''); - // Neutralize common bot trigger phrases - sanitized = neutralizeBotTriggers(sanitized); - // Trim excessive whitespace - return sanitized.trim(); - /** - * Remove unknown domains - * @param {string} s - The string to process - * @returns {string} The string with unknown domains redacted - */ - function sanitizeUrlDomains(s) { - s = s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => { - // Extract the hostname part (before first slash, colon, or other delimiter) - const hostname = domain.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; - } - /** - * Remove unknown protocols except https - * @param {string} s - The string to process - * @returns {string} The string with non-https protocols redacted - */ - function sanitizeUrlProtocols(s) { - // Match both protocol:// and protocol: patterns - // This covers URLs like https://example.com, javascript:alert(), mailto:user@domain.com, etc. - return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => { - // Allow https (case insensitive), redact everything else - return protocol.toLowerCase() === 'https' ? match : '(redacted)'; - }); - } - /** - * Neutralizes @mentions by wrapping them in backticks - * @param {string} s - The string to process - * @returns {string} The string with neutralized mentions - */ - function neutralizeMentions(s) { - // Replace @name or @org/team outside code with `@name` - return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, - (_m, p1, p2) => `${p1}\`@${p2}\``); - } - /** - * Neutralizes bot trigger phrases by wrapping them in backticks - * @param {string} s - The string to process - * @returns {string} The string with neutralized bot triggers - */ - function neutralizeBotTriggers(s) { - // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc. - return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi, - (match, action, ref) => `\`${action} #${ref}\``); - } - } - async function main() { - const fs = require("fs"); - const outputFile = process.env.GITHUB_AW_OUTPUT; - if (!outputFile) { - console.log('GITHUB_AW_OUTPUT not set, no output to collect'); - core.setOutput('output', ''); - return; - } - if (!fs.existsSync(outputFile)) { - console.log('Output file does not exist:', outputFile); - core.setOutput('output', ''); - return; - } - const outputContent = fs.readFileSync(outputFile, 'utf8'); - if (outputContent.trim() === '') { - console.log('Output file is empty'); - core.setOutput('output', ''); - } else { - const sanitizedContent = sanitizeContent(outputContent); - console.log('Collected agentic output (sanitized):', sanitizedContent.substring(0, 200) + (sanitizedContent.length > 200 ? '...' : '')); - core.setOutput('output', sanitizedContent); - } - } - await main(); - - name: Print agent output to step summary - env: - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} - run: | - echo "## Agent Output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '``````markdown' >> $GITHUB_STEP_SUMMARY - cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY - echo '``````' >> $GITHUB_STEP_SUMMARY - - name: Upload agentic output file - if: always() && steps.collect_output.outputs.output != '' - uses: actions/upload-artifact@v4 - with: - name: aw_output.txt - path: ${{ env.GITHUB_AW_OUTPUT }} - if-no-files-found: warn - name: Upload engine output files if: always() uses: actions/upload-artifact@v4 @@ -520,66 +330,4 @@ jobs: name: agentic-triage.log path: /tmp/agentic-triage.log if-no-files-found: warn - - name: Generate git patch - if: always() - run: | - # Check current git status - echo "Current git status:" - git status - # Get the initial commit SHA from the base branch of the pull request - if [ "$GITHUB_EVENT_NAME" = "pull_request" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ]; then - INITIAL_SHA="$GITHUB_BASE_REF" - else - INITIAL_SHA="$GITHUB_SHA" - fi - echo "Base commit SHA: $INITIAL_SHA" - # Configure git user for GitHub Actions - git config --global user.email "action@github.com" - git config --global user.name "GitHub Action" - # Stage any unstaged files - git add -A || true - # Check if there are staged files to commit - if ! git diff --cached --quiet; then - echo "Staged files found, committing them..." - git commit -m "[agent] staged files" || true - echo "Staged files committed" - else - echo "No staged files to commit" - fi - # Check updated git status - echo "Updated git status after committing staged files:" - git status - # Show compact diff information between initial commit and HEAD (committed changes only) - echo '## Git diff' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - git diff --name-only "$INITIAL_SHA"..HEAD >> $GITHUB_STEP_SUMMARY || true - echo '```' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - # Check if there are any committed changes since the initial commit - if git diff --quiet "$INITIAL_SHA" HEAD; then - echo "No committed changes detected since initial commit" - echo "Skipping patch generation - no committed changes to create patch from" - else - echo "Committed changes detected, generating patch..." - # Generate patch from initial commit to HEAD (committed changes only) - git format-patch "$INITIAL_SHA"..HEAD --stdout > /tmp/aw.patch || echo "Failed to generate patch" > /tmp/aw.patch - echo "Patch file created at /tmp/aw.patch" - ls -la /tmp/aw.patch - # Show the first 50 lines of the patch for review - echo '## Git Patch' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - echo '```diff' >> $GITHUB_STEP_SUMMARY - head -50 /tmp/aw.patch >> $GITHUB_STEP_SUMMARY || echo "Could not display patch contents" >> $GITHUB_STEP_SUMMARY - echo '...' >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - fi - - name: Upload git patch - if: always() - uses: actions/upload-artifact@v4 - with: - name: aw.patch - path: /tmp/aw.patch - if-no-files-found: ignore diff --git a/.github/workflows/test-codex.lock.yml b/.github/workflows/test-codex.lock.yml index c907fb4b768..e229dba273e 100644 --- a/.github/workflows/test-codex.lock.yml +++ b/.github/workflows/test-codex.lock.yml @@ -24,8 +24,6 @@ jobs: contents: read issues: read pull-requests: write - outputs: - output: ${{ steps.collect_output.outputs.output }} steps: - name: Checkout repository uses: actions/checkout@v5 @@ -35,31 +33,6 @@ jobs: node-version: '24' - name: Install Codex run: npm install -g @openai/codex - - name: Setup agent output - id: setup_agent_output - uses: actions/github-script@v7 - with: - script: | - function main() { - const fs = require('fs'); - const crypto = require('crypto'); - // Generate a random filename for the output file - const randomId = crypto.randomBytes(8).toString('hex'); - const outputFile = `/tmp/aw_output_${randomId}.txt`; - // Ensure the /tmp directory exists and create empty output file - fs.mkdirSync('/tmp', { recursive: true }); - fs.writeFileSync(outputFile, '', { mode: 0o644 }); - // Verify the file was created and is writable - if (!fs.existsSync(outputFile)) { - throw new Error(`Failed to create output file: ${outputFile}`); - } - // Set the environment variable for subsequent steps - core.exportVariable('GITHUB_AW_OUTPUT', outputFile); - console.log('Created agentic output file:', outputFile); - // Also set as step output for reference - core.setOutput('output_file', outputFile); - } - main(); - name: Setup MCPs run: | mkdir -p /tmp/mcp-config @@ -92,8 +65,6 @@ jobs: env = { "LOCAL_TIMEZONE" = "${LOCAL_TIMEZONE}" } EOF - name: Create prompt - env: - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} run: | mkdir -p /tmp/aw-prompts cat > /tmp/aw-prompts/prompt.txt << 'EOF' @@ -206,10 +177,6 @@ jobs: > AI-generated content by [${{ github.workflow }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) may contain mistakes. ``` - - --- - - **IMPORTANT**: If you need to provide output that should be captured as a workflow output variable, write it to the file "${{ env.GITHUB_AW_OUTPUT }}". This file is available for you to write any output that should be exposed from this workflow. The content of this file will be made available as the 'output' workflow output. EOF - name: Print prompt to step summary run: | @@ -271,7 +238,6 @@ jobs: env: GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} - name: Check if workflow-complete.txt exists, if so upload it id: check_file run: | @@ -288,160 +254,6 @@ jobs: with: name: workflow-complete path: workflow-complete.txt - - name: Collect agent output - id: collect_output - uses: actions/github-script@v7 - with: - script: | - /** - * Sanitizes content for safe output in GitHub Actions - * @param {string} content - The content to sanitize - * @returns {string} The sanitized content - */ - function sanitizeContent(content) { - if (!content || typeof content !== 'string') { - return ''; - } - // Read allowed domains from environment variable - const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS; - const defaultAllowedDomains = [ - 'github.com', - 'github.io', - 'githubusercontent.com', - 'githubassets.com', - 'github.dev', - 'codespaces.new' - ]; - const allowedDomains = allowedDomainsEnv - ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d) - : defaultAllowedDomains; - let sanitized = content; - // Neutralize @mentions to prevent unintended notifications - sanitized = neutralizeMentions(sanitized); - // Remove control characters (except newlines and tabs) - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); - // XML character escaping - sanitized = sanitized - .replace(/&/g, '&') // Must be first to avoid double-escaping - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - // URI filtering - replace non-https protocols with "(redacted)" - // Step 1: Temporarily mark HTTPS URLs to protect them - sanitized = sanitizeUrlProtocols(sanitized); - // Domain filtering for HTTPS URIs - // Match https:// URIs and check if domain is in allowlist - sanitized = sanitizeUrlDomains(sanitized); - // Limit total length to prevent DoS (0.5MB max) - const maxLength = 524288; - if (sanitized.length > maxLength) { - sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]'; - } - // Limit number of lines to prevent log flooding (65k max) - const lines = sanitized.split('\n'); - const maxLines = 65000; - if (lines.length > maxLines) { - sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]'; - } - // Remove ANSI escape sequences - sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ''); - // Neutralize common bot trigger phrases - sanitized = neutralizeBotTriggers(sanitized); - // Trim excessive whitespace - return sanitized.trim(); - /** - * Remove unknown domains - * @param {string} s - The string to process - * @returns {string} The string with unknown domains redacted - */ - function sanitizeUrlDomains(s) { - s = s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => { - // Extract the hostname part (before first slash, colon, or other delimiter) - const hostname = domain.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; - } - /** - * Remove unknown protocols except https - * @param {string} s - The string to process - * @returns {string} The string with non-https protocols redacted - */ - function sanitizeUrlProtocols(s) { - // Match both protocol:// and protocol: patterns - // This covers URLs like https://example.com, javascript:alert(), mailto:user@domain.com, etc. - return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => { - // Allow https (case insensitive), redact everything else - return protocol.toLowerCase() === 'https' ? match : '(redacted)'; - }); - } - /** - * Neutralizes @mentions by wrapping them in backticks - * @param {string} s - The string to process - * @returns {string} The string with neutralized mentions - */ - function neutralizeMentions(s) { - // Replace @name or @org/team outside code with `@name` - return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, - (_m, p1, p2) => `${p1}\`@${p2}\``); - } - /** - * Neutralizes bot trigger phrases by wrapping them in backticks - * @param {string} s - The string to process - * @returns {string} The string with neutralized bot triggers - */ - function neutralizeBotTriggers(s) { - // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc. - return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi, - (match, action, ref) => `\`${action} #${ref}\``); - } - } - async function main() { - const fs = require("fs"); - const outputFile = process.env.GITHUB_AW_OUTPUT; - if (!outputFile) { - console.log('GITHUB_AW_OUTPUT not set, no output to collect'); - core.setOutput('output', ''); - return; - } - if (!fs.existsSync(outputFile)) { - console.log('Output file does not exist:', outputFile); - core.setOutput('output', ''); - return; - } - const outputContent = fs.readFileSync(outputFile, 'utf8'); - if (outputContent.trim() === '') { - console.log('Output file is empty'); - core.setOutput('output', ''); - } else { - const sanitizedContent = sanitizeContent(outputContent); - console.log('Collected agentic output (sanitized):', sanitizedContent.substring(0, 200) + (sanitizedContent.length > 200 ? '...' : '')); - core.setOutput('output', sanitizedContent); - } - } - await main(); - - name: Print agent output to step summary - env: - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} - run: | - echo "## Agent Output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '``````markdown' >> $GITHUB_STEP_SUMMARY - cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY - echo '``````' >> $GITHUB_STEP_SUMMARY - - name: Upload agentic output file - if: always() && steps.collect_output.outputs.output != '' - uses: actions/upload-artifact@v4 - with: - name: aw_output.txt - path: ${{ env.GITHUB_AW_OUTPUT }} - if-no-files-found: warn - name: Upload agent logs if: always() uses: actions/upload-artifact@v4 @@ -449,66 +261,4 @@ jobs: name: test-codex.log path: /tmp/test-codex.log if-no-files-found: warn - - name: Generate git patch - if: always() - run: | - # Check current git status - echo "Current git status:" - git status - # Get the initial commit SHA from the base branch of the pull request - if [ "$GITHUB_EVENT_NAME" = "pull_request" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ]; then - INITIAL_SHA="$GITHUB_BASE_REF" - else - INITIAL_SHA="$GITHUB_SHA" - fi - echo "Base commit SHA: $INITIAL_SHA" - # Configure git user for GitHub Actions - git config --global user.email "action@github.com" - git config --global user.name "GitHub Action" - # Stage any unstaged files - git add -A || true - # Check if there are staged files to commit - if ! git diff --cached --quiet; then - echo "Staged files found, committing them..." - git commit -m "[agent] staged files" || true - echo "Staged files committed" - else - echo "No staged files to commit" - fi - # Check updated git status - echo "Updated git status after committing staged files:" - git status - # Show compact diff information between initial commit and HEAD (committed changes only) - echo '## Git diff' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - git diff --name-only "$INITIAL_SHA"..HEAD >> $GITHUB_STEP_SUMMARY || true - echo '```' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - # Check if there are any committed changes since the initial commit - if git diff --quiet "$INITIAL_SHA" HEAD; then - echo "No committed changes detected since initial commit" - echo "Skipping patch generation - no committed changes to create patch from" - else - echo "Committed changes detected, generating patch..." - # Generate patch from initial commit to HEAD (committed changes only) - git format-patch "$INITIAL_SHA"..HEAD --stdout > /tmp/aw.patch || echo "Failed to generate patch" > /tmp/aw.patch - echo "Patch file created at /tmp/aw.patch" - ls -la /tmp/aw.patch - # Show the first 50 lines of the patch for review - echo '## Git Patch' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - echo '```diff' >> $GITHUB_STEP_SUMMARY - head -50 /tmp/aw.patch >> $GITHUB_STEP_SUMMARY || echo "Could not display patch contents" >> $GITHUB_STEP_SUMMARY - echo '...' >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - fi - - name: Upload git patch - if: always() - uses: actions/upload-artifact@v4 - with: - name: aw.patch - path: /tmp/aw.patch - if-no-files-found: ignore diff --git a/.github/workflows/weekly-research.lock.yml b/.github/workflows/weekly-research.lock.yml index 04cf34ba5f3..07316ee2825 100644 --- a/.github/workflows/weekly-research.lock.yml +++ b/.github/workflows/weekly-research.lock.yml @@ -27,36 +27,9 @@ jobs: models: read pull-requests: read statuses: read - outputs: - output: ${{ steps.collect_output.outputs.output }} steps: - name: Checkout repository uses: actions/checkout@v5 - - name: Setup agent output - id: setup_agent_output - uses: actions/github-script@v7 - with: - script: | - function main() { - const fs = require('fs'); - const crypto = require('crypto'); - // Generate a random filename for the output file - const randomId = crypto.randomBytes(8).toString('hex'); - const outputFile = `/tmp/aw_output_${randomId}.txt`; - // Ensure the /tmp directory exists and create empty output file - fs.mkdirSync('/tmp', { recursive: true }); - fs.writeFileSync(outputFile, '', { mode: 0o644 }); - // Verify the file was created and is writable - if (!fs.existsSync(outputFile)) { - throw new Error(`Failed to create output file: ${outputFile}`); - } - // Set the environment variable for subsequent steps - core.exportVariable('GITHUB_AW_OUTPUT', outputFile); - console.log('Created agentic output file:', outputFile); - // Also set as step output for reference - core.setOutput('output_file', outputFile); - } - main(); - name: Setup MCPs run: | mkdir -p /tmp/mcp-config @@ -81,8 +54,6 @@ jobs: } EOF - name: Create prompt - env: - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} run: | mkdir -p /tmp/aw-prompts cat > /tmp/aw-prompts/prompt.txt << 'EOF' @@ -168,10 +139,6 @@ jobs: > NOTE: If you are refused permission to run an MCP tool or particular 'bash' commands, or need to request access to other tools or resources, then please include a request for access in the output, explaining the exact name of the tool and/or the exact prefix of bash commands needed, or other resources you need access to. - - --- - - **IMPORTANT**: If you need to provide output that should be captured as a workflow output variable, write it to the file "${{ env.GITHUB_AW_OUTPUT }}". This file is available for you to write any output that should be exposed from this workflow. The content of this file will be made available as the 'output' workflow output. EOF - name: Print prompt to step summary run: | @@ -286,12 +253,9 @@ jobs: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} claude_env: | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} mcp_config: /tmp/mcp-config/mcp-servers.json prompt_file: /tmp/aw-prompts/prompt.txt timeout_minutes: 15 - env: - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} - name: Capture Agentic Action logs if: always() run: | @@ -320,160 +284,6 @@ jobs: with: name: workflow-complete path: workflow-complete.txt - - name: Collect agent output - id: collect_output - uses: actions/github-script@v7 - with: - script: | - /** - * Sanitizes content for safe output in GitHub Actions - * @param {string} content - The content to sanitize - * @returns {string} The sanitized content - */ - function sanitizeContent(content) { - if (!content || typeof content !== 'string') { - return ''; - } - // Read allowed domains from environment variable - const allowedDomainsEnv = process.env.GITHUB_AW_ALLOWED_DOMAINS; - const defaultAllowedDomains = [ - 'github.com', - 'github.io', - 'githubusercontent.com', - 'githubassets.com', - 'github.dev', - 'codespaces.new' - ]; - const allowedDomains = allowedDomainsEnv - ? allowedDomainsEnv.split(',').map(d => d.trim()).filter(d => d) - : defaultAllowedDomains; - let sanitized = content; - // Neutralize @mentions to prevent unintended notifications - sanitized = neutralizeMentions(sanitized); - // Remove control characters (except newlines and tabs) - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); - // XML character escaping - sanitized = sanitized - .replace(/&/g, '&') // Must be first to avoid double-escaping - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - // URI filtering - replace non-https protocols with "(redacted)" - // Step 1: Temporarily mark HTTPS URLs to protect them - sanitized = sanitizeUrlProtocols(sanitized); - // Domain filtering for HTTPS URIs - // Match https:// URIs and check if domain is in allowlist - sanitized = sanitizeUrlDomains(sanitized); - // Limit total length to prevent DoS (0.5MB max) - const maxLength = 524288; - if (sanitized.length > maxLength) { - sanitized = sanitized.substring(0, maxLength) + '\n[Content truncated due to length]'; - } - // Limit number of lines to prevent log flooding (65k max) - const lines = sanitized.split('\n'); - const maxLines = 65000; - if (lines.length > maxLines) { - sanitized = lines.slice(0, maxLines).join('\n') + '\n[Content truncated due to line count]'; - } - // Remove ANSI escape sequences - sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ''); - // Neutralize common bot trigger phrases - sanitized = neutralizeBotTriggers(sanitized); - // Trim excessive whitespace - return sanitized.trim(); - /** - * Remove unknown domains - * @param {string} s - The string to process - * @returns {string} The string with unknown domains redacted - */ - function sanitizeUrlDomains(s) { - s = s.replace(/\bhttps:\/\/([^\/\s\])}'"<>&\x00-\x1f]+)/gi, (match, domain) => { - // Extract the hostname part (before first slash, colon, or other delimiter) - const hostname = domain.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; - } - /** - * Remove unknown protocols except https - * @param {string} s - The string to process - * @returns {string} The string with non-https protocols redacted - */ - function sanitizeUrlProtocols(s) { - // Match both protocol:// and protocol: patterns - // This covers URLs like https://example.com, javascript:alert(), mailto:user@domain.com, etc. - return s.replace(/\b(\w+):(?:\/\/)?[^\s\])}'"<>&\x00-\x1f]+/gi, (match, protocol) => { - // Allow https (case insensitive), redact everything else - return protocol.toLowerCase() === 'https' ? match : '(redacted)'; - }); - } - /** - * Neutralizes @mentions by wrapping them in backticks - * @param {string} s - The string to process - * @returns {string} The string with neutralized mentions - */ - function neutralizeMentions(s) { - // Replace @name or @org/team outside code with `@name` - return s.replace(/(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, - (_m, p1, p2) => `${p1}\`@${p2}\``); - } - /** - * Neutralizes bot trigger phrases by wrapping them in backticks - * @param {string} s - The string to process - * @returns {string} The string with neutralized bot triggers - */ - function neutralizeBotTriggers(s) { - // Neutralize common bot trigger phrases like "fixes #123", "closes #asdfs", etc. - return s.replace(/\b(fixes?|closes?|resolves?|fix|close|resolve)\s+#(\w+)/gi, - (match, action, ref) => `\`${action} #${ref}\``); - } - } - async function main() { - const fs = require("fs"); - const outputFile = process.env.GITHUB_AW_OUTPUT; - if (!outputFile) { - console.log('GITHUB_AW_OUTPUT not set, no output to collect'); - core.setOutput('output', ''); - return; - } - if (!fs.existsSync(outputFile)) { - console.log('Output file does not exist:', outputFile); - core.setOutput('output', ''); - return; - } - const outputContent = fs.readFileSync(outputFile, 'utf8'); - if (outputContent.trim() === '') { - console.log('Output file is empty'); - core.setOutput('output', ''); - } else { - const sanitizedContent = sanitizeContent(outputContent); - console.log('Collected agentic output (sanitized):', sanitizedContent.substring(0, 200) + (sanitizedContent.length > 200 ? '...' : '')); - core.setOutput('output', sanitizedContent); - } - } - await main(); - - name: Print agent output to step summary - env: - GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }} - run: | - echo "## Agent Output" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '``````markdown' >> $GITHUB_STEP_SUMMARY - cat ${{ env.GITHUB_AW_OUTPUT }} >> $GITHUB_STEP_SUMMARY - echo '``````' >> $GITHUB_STEP_SUMMARY - - name: Upload agentic output file - if: always() && steps.collect_output.outputs.output != '' - uses: actions/upload-artifact@v4 - with: - name: aw_output.txt - path: ${{ env.GITHUB_AW_OUTPUT }} - if-no-files-found: warn - name: Upload engine output files if: always() uses: actions/upload-artifact@v4 @@ -489,66 +299,4 @@ jobs: name: weekly-research.log path: /tmp/weekly-research.log if-no-files-found: warn - - name: Generate git patch - if: always() - run: | - # Check current git status - echo "Current git status:" - git status - # Get the initial commit SHA from the base branch of the pull request - if [ "$GITHUB_EVENT_NAME" = "pull_request" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ]; then - INITIAL_SHA="$GITHUB_BASE_REF" - else - INITIAL_SHA="$GITHUB_SHA" - fi - echo "Base commit SHA: $INITIAL_SHA" - # Configure git user for GitHub Actions - git config --global user.email "action@github.com" - git config --global user.name "GitHub Action" - # Stage any unstaged files - git add -A || true - # Check if there are staged files to commit - if ! git diff --cached --quiet; then - echo "Staged files found, committing them..." - git commit -m "[agent] staged files" || true - echo "Staged files committed" - else - echo "No staged files to commit" - fi - # Check updated git status - echo "Updated git status after committing staged files:" - git status - # Show compact diff information between initial commit and HEAD (committed changes only) - echo '## Git diff' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - git diff --name-only "$INITIAL_SHA"..HEAD >> $GITHUB_STEP_SUMMARY || true - echo '```' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - # Check if there are any committed changes since the initial commit - if git diff --quiet "$INITIAL_SHA" HEAD; then - echo "No committed changes detected since initial commit" - echo "Skipping patch generation - no committed changes to create patch from" - else - echo "Committed changes detected, generating patch..." - # Generate patch from initial commit to HEAD (committed changes only) - git format-patch "$INITIAL_SHA"..HEAD --stdout > /tmp/aw.patch || echo "Failed to generate patch" > /tmp/aw.patch - echo "Patch file created at /tmp/aw.patch" - ls -la /tmp/aw.patch - # Show the first 50 lines of the patch for review - echo '## Git Patch' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - echo '```diff' >> $GITHUB_STEP_SUMMARY - head -50 /tmp/aw.patch >> $GITHUB_STEP_SUMMARY || echo "Could not display patch contents" >> $GITHUB_STEP_SUMMARY - echo '...' >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo '' >> $GITHUB_STEP_SUMMARY - fi - - name: Upload git patch - if: always() - uses: actions/upload-artifact@v4 - with: - name: aw.patch - path: /tmp/aw.patch - if-no-files-found: ignore diff --git a/pkg/workflow/agentic_engine.go b/pkg/workflow/agentic_engine.go index 9c6353d7c3b..9d6b8ad84d3 100644 --- a/pkg/workflow/agentic_engine.go +++ b/pkg/workflow/agentic_engine.go @@ -40,7 +40,7 @@ type AgenticEngine interface { GetInstallationSteps(engineConfig *EngineConfig) []GitHubActionStep // GetExecutionConfig returns the configuration for executing this engine - GetExecutionConfig(workflowName string, logFile string, engineConfig *EngineConfig) ExecutionConfig + GetExecutionConfig(workflowName string, logFile string, engineConfig *EngineConfig, hasOutput bool) ExecutionConfig // RenderMCPConfig renders the MCP configuration for this engine to the given YAML builder RenderMCPConfig(yaml *strings.Builder, tools map[string]any, mcpTools []string) diff --git a/pkg/workflow/agentic_output_test.go b/pkg/workflow/agentic_output_test.go index ff22b8b40d4..601749d038d 100644 --- a/pkg/workflow/agentic_output_test.go +++ b/pkg/workflow/agentic_output_test.go @@ -26,6 +26,9 @@ tools: github: allowed: [list_issues] engine: claude +output: + labels: + allowed: ["bug", "enhancement"] --- # Test Agentic Output Collection @@ -118,6 +121,9 @@ tools: github: allowed: [list_issues] engine: codex +output: + labels: + allowed: ["bug", "enhancement"] --- # Test Codex No Engine Output Collection diff --git a/pkg/workflow/claude_engine.go b/pkg/workflow/claude_engine.go index a1e53183ad8..765ef46f5ca 100644 --- a/pkg/workflow/claude_engine.go +++ b/pkg/workflow/claude_engine.go @@ -58,13 +58,19 @@ func (e *ClaudeEngine) GetDeclaredOutputFiles() []string { return []string{"output.txt"} } -func (e *ClaudeEngine) GetExecutionConfig(workflowName string, logFile string, engineConfig *EngineConfig) ExecutionConfig { +func (e *ClaudeEngine) GetExecutionConfig(workflowName string, logFile string, engineConfig *EngineConfig, hasOutput bool) ExecutionConfig { // Determine the action version to use actionVersion := DefaultClaudeActionVersion // Default version if engineConfig != nil && engineConfig.Version != "" { actionVersion = engineConfig.Version } + // Build claude_env based on hasOutput parameter + claudeEnv := "|\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}" + if hasOutput { + claudeEnv += "\n GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }}" + } + config := ExecutionConfig{ StepName: "Execute Claude Code Action", Action: fmt.Sprintf("anthropics/claude-code-base-action@%s", actionVersion), @@ -72,14 +78,11 @@ func (e *ClaudeEngine) GetExecutionConfig(workflowName string, logFile string, e "prompt_file": "/tmp/aw-prompts/prompt.txt", "anthropic_api_key": "${{ secrets.ANTHROPIC_API_KEY }}", "mcp_config": "/tmp/mcp-config/mcp-servers.json", - "claude_env": "|\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }}", + "claude_env": claudeEnv, "allowed_tools": "", // Will be filled in during generation "timeout_minutes": "", // Will be filled in during generation "max_turns": "", // Will be filled in during generation }, - Environment: map[string]string{ - "GH_TOKEN": "${{ secrets.GITHUB_TOKEN }}", - }, } // Add model configuration if specified diff --git a/pkg/workflow/claude_engine_network_test.go b/pkg/workflow/claude_engine_network_test.go index 64e5324c2f3..0ad521616ba 100644 --- a/pkg/workflow/claude_engine_network_test.go +++ b/pkg/workflow/claude_engine_network_test.go @@ -70,7 +70,7 @@ func TestClaudeEngineNetworkPermissions(t *testing.T) { Model: "claude-3-5-sonnet-20241022", } - execConfig := engine.GetExecutionConfig("test-workflow", "test-log", config) + execConfig := engine.GetExecutionConfig("test-workflow", "test-log", config, false) // Verify settings parameter is not present if settings, exists := execConfig.Inputs["settings"]; exists { @@ -94,7 +94,7 @@ func TestClaudeEngineNetworkPermissions(t *testing.T) { }, } - execConfig := engine.GetExecutionConfig("test-workflow", "test-log", config) + execConfig := engine.GetExecutionConfig("test-workflow", "test-log", config, false) // Verify settings parameter is present if settings, exists := execConfig.Inputs["settings"]; !exists { @@ -128,7 +128,7 @@ func TestClaudeEngineNetworkPermissions(t *testing.T) { }, } - execConfig := engine.GetExecutionConfig("test-workflow", "test-log", config) + execConfig := engine.GetExecutionConfig("test-workflow", "test-log", config, false) // With empty allowed list, we should enforce deny-all policy via settings if settings, exists := execConfig.Inputs["settings"]; !exists { @@ -149,7 +149,7 @@ func TestClaudeEngineNetworkPermissions(t *testing.T) { }, } - execConfig := engine.GetExecutionConfig("test-workflow", "test-log", config) + execConfig := engine.GetExecutionConfig("test-workflow", "test-log", config, false) // Verify action version uses config version expectedAction := "anthropics/claude-code-base-action@v1.2.3" @@ -201,7 +201,7 @@ func TestNetworkPermissionsIntegration(t *testing.T) { } // Get execution config - execConfig := engine.GetExecutionConfig("test-workflow", "test-log", config) + execConfig := engine.GetExecutionConfig("test-workflow", "test-log", config, false) if execConfig.Inputs["settings"] != ".claude/settings.json" { t.Error("Execution config should reference generated settings file") } @@ -236,8 +236,8 @@ func TestNetworkPermissionsIntegration(t *testing.T) { t.Error("Different engine instances should generate same number of steps") } - execConfig1 := engine1.GetExecutionConfig("test", "log", config) - execConfig2 := engine2.GetExecutionConfig("test", "log", config) + execConfig1 := engine1.GetExecutionConfig("test", "log", config, false) + execConfig2 := engine2.GetExecutionConfig("test", "log", config, false) if execConfig1.Inputs["settings"] != execConfig2.Inputs["settings"] { t.Error("Different engine instances should generate consistent execution configs") diff --git a/pkg/workflow/claude_engine_test.go b/pkg/workflow/claude_engine_test.go index b45a6275b07..bc33f127c13 100644 --- a/pkg/workflow/claude_engine_test.go +++ b/pkg/workflow/claude_engine_test.go @@ -36,7 +36,7 @@ func TestClaudeEngine(t *testing.T) { } // Test execution config - config := engine.GetExecutionConfig("test-workflow", "test-log", nil) + config := engine.GetExecutionConfig("test-workflow", "test-log", nil, false) if config.StepName != "Execute Claude Code Action" { t.Errorf("Expected step name 'Execute Claude Code Action', got '%s'", config.StepName) } @@ -62,7 +62,7 @@ func TestClaudeEngine(t *testing.T) { t.Errorf("Expected mcp_config input, got '%s'", config.Inputs["mcp_config"]) } - expectedClaudeEnv := "|\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }}" + expectedClaudeEnv := "|\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}" if config.Inputs["claude_env"] != expectedClaudeEnv { t.Errorf("Expected claude_env input '%s', got '%s'", expectedClaudeEnv, config.Inputs["claude_env"]) } @@ -79,10 +79,18 @@ func TestClaudeEngine(t *testing.T) { if _, hasMaxTurns := config.Inputs["max_turns"]; !hasMaxTurns { t.Error("Expected max_turns input to be present") } +} + +func TestClaudeEngineWithOutput(t *testing.T) { + engine := NewClaudeEngine() - // Check environment variables - if config.Environment["GH_TOKEN"] != "${{ secrets.GITHUB_TOKEN }}" { - t.Errorf("Expected GH_TOKEN environment variable, got '%s'", config.Environment["GH_TOKEN"]) + // Test execution config with hasOutput=true + config := engine.GetExecutionConfig("test-workflow", "test-log", nil, true) + + // Should include GITHUB_AW_OUTPUT when hasOutput=true + expectedClaudeEnv := "|\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }}" + if config.Inputs["claude_env"] != expectedClaudeEnv { + t.Errorf("Expected claude_env input with output '%s', got '%s'", expectedClaudeEnv, config.Inputs["claude_env"]) } } @@ -101,7 +109,7 @@ func TestClaudeEngineConfiguration(t *testing.T) { for _, tc := range testCases { t.Run(tc.workflowName, func(t *testing.T) { - config := engine.GetExecutionConfig(tc.workflowName, tc.logFile, nil) + config := engine.GetExecutionConfig(tc.workflowName, tc.logFile, nil, false) // Verify the configuration is consistent regardless of input if config.StepName != "Execute Claude Code Action" { @@ -133,7 +141,7 @@ func TestClaudeEngineWithVersion(t *testing.T) { Model: "claude-3-5-sonnet-20241022", } - config := engine.GetExecutionConfig("test-workflow", "test-log", engineConfig) + config := engine.GetExecutionConfig("test-workflow", "test-log", engineConfig, false) // Check that the version is correctly used in the action expectedAction := "anthropics/claude-code-base-action@v1.2.3" @@ -156,7 +164,7 @@ func TestClaudeEngineWithoutVersion(t *testing.T) { Model: "claude-3-5-sonnet-20241022", } - config := engine.GetExecutionConfig("test-workflow", "test-log", engineConfig) + config := engine.GetExecutionConfig("test-workflow", "test-log", engineConfig, false) // Check that default version is used expectedAction := fmt.Sprintf("anthropics/claude-code-base-action@%s", DefaultClaudeActionVersion) diff --git a/pkg/workflow/codex_engine.go b/pkg/workflow/codex_engine.go index c3adfeacee9..27d48a8d1cf 100644 --- a/pkg/workflow/codex_engine.go +++ b/pkg/workflow/codex_engine.go @@ -46,7 +46,7 @@ func (e *CodexEngine) GetInstallationSteps(engineConfig *EngineConfig) []GitHubA } } -func (e *CodexEngine) GetExecutionConfig(workflowName string, logFile string, engineConfig *EngineConfig) ExecutionConfig { +func (e *CodexEngine) GetExecutionConfig(workflowName string, logFile string, engineConfig *EngineConfig, hasOutput bool) ExecutionConfig { // Use model from engineConfig if available, otherwise default to o4-mini model := "o4-mini" if engineConfig != nil && engineConfig.Model != "" { @@ -64,13 +64,20 @@ codex exec \ -c model=%s \ --full-auto "$INSTRUCTION" 2>&1 | tee %s`, model, logFile) + env := map[string]string{ + "OPENAI_API_KEY": "${{ secrets.OPENAI_API_KEY }}", + "GITHUB_STEP_SUMMARY": "${{ env.GITHUB_STEP_SUMMARY }}", + } + + // Add GITHUB_AW_OUTPUT if output is needed + if hasOutput { + env["GITHUB_AW_OUTPUT"] = "${{ env.GITHUB_AW_OUTPUT }}" + } + return ExecutionConfig{ - StepName: "Run Codex", - Command: command, - Environment: map[string]string{ - "OPENAI_API_KEY": "${{ secrets.OPENAI_API_KEY }}", - "GITHUB_STEP_SUMMARY": "${{ env.GITHUB_STEP_SUMMARY }}", - }, + StepName: "Run Codex", + Command: command, + Environment: env, } } diff --git a/pkg/workflow/codex_engine_test.go b/pkg/workflow/codex_engine_test.go index 462b037a41b..b0ca45238ce 100644 --- a/pkg/workflow/codex_engine_test.go +++ b/pkg/workflow/codex_engine_test.go @@ -47,7 +47,7 @@ func TestCodexEngine(t *testing.T) { } // Test execution config - config := engine.GetExecutionConfig("test-workflow", "test-log", nil) + config := engine.GetExecutionConfig("test-workflow", "test-log", nil, false) if config.StepName != "Run Codex" { t.Errorf("Expected step name 'Run Codex', got '%s'", config.StepName) } diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 30db8a5fa2f..ad5915023f9 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -129,7 +129,6 @@ type WorkflowData struct { AllowedTools string AI string // "claude" or "codex" (for backwards compatibility) EngineConfig *EngineConfig // Extended engine configuration - MaxTurns string StopTime string Alias string // for @alias trigger support AliasOtherEvents map[string]any // for merging alias with other events @@ -578,11 +577,6 @@ func (c *Compiler) parseWorkflowFile(markdownPath string) (*WorkflowData, error) workflowData.RunsOn = c.extractTopLevelYAMLSection(result.Frontmatter, "runs-on") workflowData.Cache = c.extractTopLevelYAMLSection(result.Frontmatter, "cache") - // Extract max-turns from engine config instead of top-level frontmatter - if engineConfig != nil && engineConfig.MaxTurns != "" { - workflowData.MaxTurns = engineConfig.MaxTurns - } - // Extract stop-after from the on: section stopAfter, err := c.extractStopAfterFromOn(result.Frontmatter) if err != nil { @@ -1887,8 +1881,12 @@ func (c *Compiler) buildMainJob(data *WorkflowData, jobName string, taskJobCreat } // Build outputs for all engines (GITHUB_AW_OUTPUT functionality) - outputs := map[string]string{ - "output": "${{ steps.collect_output.outputs.output }}", + // Only include output if the workflow actually uses the output feature + var outputs map[string]string + if data.Output != nil { + outputs = map[string]string{ + "output": "${{ steps.collect_output.outputs.output }}", + } } job := &Job{ @@ -2098,8 +2096,10 @@ func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowDat } } - // Generate output file setup step for all engines (GITHUB_AW_OUTPUT functionality) - c.generateOutputFileSetup(yaml, data) + // Generate output file setup step only if output feature is used (GITHUB_AW_OUTPUT functionality) + if data.Output != nil { + c.generateOutputFileSetup(yaml, data) + } // Add MCP setup c.generateMCPSetup(yaml, data.Tools, engine) @@ -2125,8 +2125,10 @@ func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowDat // add workflow_complete.txt c.generateWorkflowComplete(yaml) - // Add output collection step for all engines (GITHUB_AW_OUTPUT functionality) - c.generateOutputCollectionStep(yaml, data) + // Add output collection step only if output feature is used (GITHUB_AW_OUTPUT functionality) + if data.Output != nil { + c.generateOutputCollectionStep(yaml, data) + } // Add engine-declared output files collection (if any) if len(engine.GetDeclaredOutputFiles()) > 0 { @@ -2136,8 +2138,10 @@ func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowDat // upload agent logs c.generateUploadAgentLogs(yaml, logFile, logFileFull) - // Add git patch generation step after agentic execution - c.generateGitPatchStep(yaml) + // Add git patch generation step only if output feature is used + if data.Output != nil { + c.generateGitPatchStep(yaml) + } // Add post-steps (if any) after AI execution c.generatePostSteps(yaml, data) @@ -2184,8 +2188,13 @@ func (c *Compiler) generateUploadAwInfo(yaml *strings.Builder) { func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData, engine AgenticEngine) { yaml.WriteString(" - name: Create prompt\n") - yaml.WriteString(" env:\n") - yaml.WriteString(" GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }}\n") + + // Only add GITHUB_AW_OUTPUT environment variable if output feature is used + if data.Output != nil { + yaml.WriteString(" env:\n") + yaml.WriteString(" GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }}\n") + } + yaml.WriteString(" run: |\n") yaml.WriteString(" mkdir -p /tmp/aw-prompts\n") yaml.WriteString(" cat > /tmp/aw-prompts/prompt.txt << 'EOF'\n") @@ -2195,11 +2204,14 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData, eng yaml.WriteString(" " + line + "\n") } - // Add output instructions for all engines (GITHUB_AW_OUTPUT functionality) - yaml.WriteString(" \n") - yaml.WriteString(" ---\n") - yaml.WriteString(" \n") - yaml.WriteString(" **IMPORTANT**: If you need to provide output that should be captured as a workflow output variable, write it to the file \"${{ env.GITHUB_AW_OUTPUT }}\". This file is available for you to write any output that should be exposed from this workflow. The content of this file will be made available as the 'output' workflow output.\n") + // Add output instructions only if output feature is used (GITHUB_AW_OUTPUT functionality) + if data.Output != nil { + yaml.WriteString(" \n") + yaml.WriteString(" ---\n") + yaml.WriteString(" \n") + yaml.WriteString(" **IMPORTANT**: If you need to provide output that should be captured as a workflow output variable, write it to the file \"${{ env.GITHUB_AW_OUTPUT }}\". This file is available for you to write any output that should be exposed from this workflow. The content of this file will be made available as the 'output' workflow output.\n") + } + yaml.WriteString(" EOF\n") // Add step to print prompt to GitHub step summary for debugging @@ -2485,7 +2497,7 @@ func (c *Compiler) convertStepToYAML(stepMap map[string]any) (string, error) { // generateEngineExecutionSteps generates the execution steps for the specified agentic engine func (c *Compiler) generateEngineExecutionSteps(yaml *strings.Builder, data *WorkflowData, engine AgenticEngine, logFile string) { - executionConfig := engine.GetExecutionConfig(data.Name, logFile, data.EngineConfig) + executionConfig := engine.GetExecutionConfig(data.Name, logFile, data.EngineConfig, data.Output != nil) if executionConfig.Command != "" { // Command-based execution (e.g., Codex) @@ -2497,27 +2509,25 @@ func (c *Compiler) generateEngineExecutionSteps(yaml *strings.Builder, data *Wor for _, line := range commandLines { yaml.WriteString(" " + line + "\n") } + env := executionConfig.Environment + if data.Output != nil { + env["GITHUB_AW_OUTPUT"] = "${{ env.GITHUB_AW_OUTPUT }}" + } // Add environment variables - if len(executionConfig.Environment) > 0 { + if len(env) > 0 { yaml.WriteString(" env:\n") // Sort environment keys for consistent output - envKeys := make([]string, 0, len(executionConfig.Environment)) - for key := range executionConfig.Environment { + envKeys := make([]string, 0, len(env)) + for key := range env { envKeys = append(envKeys, key) } sort.Strings(envKeys) for _, key := range envKeys { - value := executionConfig.Environment[key] + value := env[key] fmt.Fprintf(yaml, " %s: %s\n", key, value) } - // Add GITHUB_AW_OUTPUT environment variable for all engines - yaml.WriteString(" GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }}\n") - } else { - // Add GITHUB_AW_OUTPUT environment variable even if no other env vars - yaml.WriteString(" env:\n") - yaml.WriteString(" GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }}\n") } } else if executionConfig.Action != "" { @@ -2548,16 +2558,18 @@ func (c *Compiler) generateEngineExecutionSteps(yaml *strings.Builder, data *Wor yaml.WriteString(" " + data.TimeoutMinutes + "\n") } } else if key == "max_turns" { - if data.MaxTurns != "" { - fmt.Fprintf(yaml, " max_turns: %s\n", data.MaxTurns) + if data.EngineConfig != nil && data.EngineConfig.MaxTurns != "" { + fmt.Fprintf(yaml, " max_turns: %s\n", data.EngineConfig.MaxTurns) } } else if value != "" { fmt.Fprintf(yaml, " %s: %s\n", key, value) } } - // Add environment section to pass GITHUB_AW_OUTPUT to the action for all engines - yaml.WriteString(" env:\n") - yaml.WriteString(" GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }}\n") + // Add environment section to pass GITHUB_AW_OUTPUT to the action only if output feature is used + if data.Output != nil { + yaml.WriteString(" env:\n") + yaml.WriteString(" GITHUB_AW_OUTPUT: ${{ env.GITHUB_AW_OUTPUT }}\n") + } yaml.WriteString(" - name: Capture Agentic Action logs\n") yaml.WriteString(" if: always()\n") yaml.WriteString(" run: |\n") diff --git a/pkg/workflow/engine_config_test.go b/pkg/workflow/engine_config_test.go index 2687b564d1c..8e8101bc571 100644 --- a/pkg/workflow/engine_config_test.go +++ b/pkg/workflow/engine_config_test.go @@ -299,7 +299,7 @@ func TestEngineConfigurationWithModel(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - config := tt.engine.GetExecutionConfig("test-workflow", "test-log", tt.engineConfig) + config := tt.engine.GetExecutionConfig("test-workflow", "test-log", tt.engineConfig, false) switch tt.engine.GetID() { case "claude": @@ -330,7 +330,7 @@ func TestNilEngineConfig(t *testing.T) { for _, engine := range engines { t.Run(engine.GetID(), func(t *testing.T) { // Should not panic when engineConfig is nil - config := engine.GetExecutionConfig("test-workflow", "test-log", nil) + config := engine.GetExecutionConfig("test-workflow", "test-log", nil, false) if config.StepName == "" { t.Errorf("Expected non-empty step name for engine %s", engine.GetID()) diff --git a/pkg/workflow/git_patch_test.go b/pkg/workflow/git_patch_test.go index 289f5fc8e05..58a9545d62e 100644 --- a/pkg/workflow/git_patch_test.go +++ b/pkg/workflow/git_patch_test.go @@ -29,6 +29,9 @@ func TestGitPatchGeneration(t *testing.T) { testMarkdown := `--- on: workflow_dispatch: +output: + labels: + allowed: ["bug", "enhancement"] --- # Test Git Patch