From 61cd5e3f038ddde359d0a0268ee7e2154c8bea77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:19:05 +0000 Subject: [PATCH 1/2] Initial plan From 0d4342e4ac7628a3ce5d8450a3b6aae0266aadb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:33:22 +0000 Subject: [PATCH 2/2] fix: update github/codeql-action/upload-sarif SHA to v3 latest Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- .github/aw/actions-lock.json | 2 +- .../code-health-file-diet-campaign.lock.yml | 2405 ----------------- .../code-health-file-diet.campaign.g.lock.yml | 1 + .../daily-malicious-code-scan.lock.yml | 6 +- .../incident-response-campaign.lock.yml | 2403 ---------------- .../incident-response.campaign.g.lock.yml | 1 + .../org-modernization-campaign.lock.yml | 2405 ----------------- .../org-modernization.campaign.g.lock.yml | 1 + .../security-compliance-campaign.lock.yml | 2405 ----------------- .../security-compliance.campaign.g.lock.yml | 1 + go.mod | 12 +- pkg/workflow/data/action_pins.json | 2 +- 12 files changed, 15 insertions(+), 9629 deletions(-) delete mode 100644 .github/workflows/code-health-file-diet-campaign.lock.yml delete mode 100644 .github/workflows/incident-response-campaign.lock.yml delete mode 100644 .github/workflows/org-modernization-campaign.lock.yml delete mode 100644 .github/workflows/security-compliance-campaign.lock.yml diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index c96dafdf0af..34b4e7bbb00 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -113,7 +113,7 @@ "github/codeql-action/upload-sarif@v3": { "repo": "github/codeql-action/upload-sarif", "version": "v3", - "sha": "6d10af73d5896080e7b530ea0822d927c05661f1" + "sha": "323fb8c0ad5be63b7a6ebf1f32c35882fcfea2cf" }, "github/stale-repos@v3": { "repo": "github/stale-repos", diff --git a/.github/workflows/code-health-file-diet-campaign.lock.yml b/.github/workflows/code-health-file-diet-campaign.lock.yml deleted file mode 100644 index adbaefa73bf..00000000000 --- a/.github/workflows/code-health-file-diet-campaign.lock.yml +++ /dev/null @@ -1,2405 +0,0 @@ -# -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# 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 -# For more information: https://github.com/githubnext/gh-aw/blob/main/.github/aw/github-agentic-workflows.md -# -# Reduce oversized Go files across the codebase with tracked refactors. -# -# Job Dependency Graph: -# ```mermaid -# graph LR -# activation["activation"] -# agent["agent"] -# pre_activation["pre_activation"] -# activation --> agent -# pre_activation --> activation -# ``` -# -# Original Prompt: -# ```markdown -# # Campaign Orchestrator -# -# This workflow orchestrates the 'Campaign: File Diet' campaign. -# -# - Tracker label: `campaign:code-health-file-diet` -# - Associated workflows: daily-file-diet -# - Memory paths: memory/campaigns/code-health-file-diet-*/** -# - Metrics glob: `memory/campaigns/code-health-file-diet-*/metrics/*.json` -# -# Use these details to coordinate workers, update metrics, and track progress for this campaign. -# ``` -# -# Pinned GitHub Actions: -# - actions/github-script@v8 (ed597411d8f924073f98dfc5c65a23a2325f34cd) -# https://github.com/actions/github-script/commit/ed597411d8f924073f98dfc5c65a23a2325f34cd -# - actions/setup-node@v6 (395ad3262231945c25e8478fd5baf05154b1d79f) -# https://github.com/actions/setup-node/commit/395ad3262231945c25e8478fd5baf05154b1d79f -# - actions/upload-artifact@v5 (330a01c490aca151604b8cf639adc76d48f6c5d4) -# https://github.com/actions/upload-artifact/commit/330a01c490aca151604b8cf639adc76d48f6c5d4 - -name: "Campaign: File Diet" -on: - workflow_dispatch: - - -permissions: {} - - - - - -jobs: - activation: - needs: pre_activation - if: needs.pre_activation.outputs.activated == 'true' - runs-on: ubuntu-slim - permissions: - contents: read - outputs: - comment_id: "" - comment_repo: "" - steps: - - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_WORKFLOW_FILE: "code-health-file-diet-campaign.lock.yml" - with: - script: | - async function main() { - const workflowFile = process.env.GH_AW_WORKFLOW_FILE; - if (!workflowFile) { - core.setFailed("Configuration error: GH_AW_WORKFLOW_FILE not available."); - return; - } - const workflowBasename = workflowFile.replace(".lock.yml", ""); - const workflowMdPath = `.github/workflows/${workflowBasename}.md`; - const lockFilePath = `.github/workflows/${workflowFile}`; - core.info(`Checking workflow timestamps using GitHub API:`); - core.info(` Source: ${workflowMdPath}`); - core.info(` Lock file: ${lockFilePath}`); - const { owner, repo } = context.repo; - const ref = context.sha; - async function getLastCommitForFile(path) { - try { - const response = await github.rest.repos.listCommits({ - owner, - repo, - path, - per_page: 1, - sha: ref, - }); - if (response.data && response.data.length > 0) { - const commit = response.data[0]; - return { - sha: commit.sha, - date: commit.commit.committer.date, - message: commit.commit.message, - }; - } - return null; - } catch (error) { - core.info(`Could not fetch commit for ${path}: ${error.message}`); - return null; - } - } - const workflowCommit = await getLastCommitForFile(workflowMdPath); - const lockCommit = await getLastCommitForFile(lockFilePath); - if (!workflowCommit) { - core.info(`Source file does not exist: ${workflowMdPath}`); - } - if (!lockCommit) { - core.info(`Lock file does not exist: ${lockFilePath}`); - } - if (!workflowCommit || !lockCommit) { - core.info("Skipping timestamp check - one or both files not found"); - return; - } - const workflowDate = new Date(workflowCommit.date); - const lockDate = new Date(lockCommit.date); - core.info(` Source last commit: ${workflowDate.toISOString()} (${workflowCommit.sha.substring(0, 7)})`); - core.info(` Lock last commit: ${lockDate.toISOString()} (${lockCommit.sha.substring(0, 7)})`); - if (workflowDate > lockDate) { - const warningMessage = `WARNING: Lock file '${lockFilePath}' is outdated! The workflow file '${workflowMdPath}' has been modified more recently. Run 'gh aw compile' to regenerate the lock file.`; - core.error(warningMessage); - const workflowTimestamp = workflowDate.toISOString(); - const lockTimestamp = lockDate.toISOString(); - let summary = core.summary - .addRaw("### ⚠️ Workflow Lock File Warning\n\n") - .addRaw("**WARNING**: Lock file is outdated and needs to be regenerated.\n\n") - .addRaw("**Files:**\n") - .addRaw(`- Source: \`${workflowMdPath}\`\n`) - .addRaw(` - Last commit: ${workflowTimestamp}\n`) - .addRaw( - ` - Commit SHA: [\`${workflowCommit.sha.substring(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${workflowCommit.sha})\n` - ) - .addRaw(`- Lock: \`${lockFilePath}\`\n`) - .addRaw(` - Last commit: ${lockTimestamp}\n`) - .addRaw(` - Commit SHA: [\`${lockCommit.sha.substring(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${lockCommit.sha})\n\n`) - .addRaw("**Action Required:** Run `gh aw compile` to regenerate the lock file.\n\n"); - await summary.write(); - } else if (workflowCommit.sha === lockCommit.sha) { - core.info("✅ Lock file is up to date (same commit)"); - } else { - core.info("✅ Lock file is up to date"); - } - } - main().catch(error => { - core.setFailed(error instanceof Error ? error.message : String(error)); - }); - - agent: - needs: activation - outputs: - model: ${{ steps.generate_aw_info.outputs.model }} - steps: - - name: Create gh-aw temp directory - run: | - mkdir -p /tmp/gh-aw/agent - mkdir -p /tmp/gh-aw/sandbox/agent/logs - echo "Created /tmp/gh-aw/agent directory for agentic workflow temporary files" - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Validate COPILOT_GITHUB_TOKEN secret - run: | - if [ -z "$COPILOT_GITHUB_TOKEN" ]; then - { - echo "❌ Error: None of the following secrets are set: COPILOT_GITHUB_TOKEN" - echo "The GitHub Copilot CLI engine requires either COPILOT_GITHUB_TOKEN secret to be configured." - echo "Please configure one of these secrets in your repository settings." - echo "Documentation: https://githubnext.github.io/gh-aw/reference/engines/#github-copilot-default" - } >> "$GITHUB_STEP_SUMMARY" - echo "Error: None of the following secrets are set: COPILOT_GITHUB_TOKEN" - echo "The GitHub Copilot CLI engine requires either COPILOT_GITHUB_TOKEN secret to be configured." - echo "Please configure one of these secrets in your repository settings." - echo "Documentation: https://githubnext.github.io/gh-aw/reference/engines/#github-copilot-default" - exit 1 - fi - - # Log success to stdout (not step summary) - if [ -n "$COPILOT_GITHUB_TOKEN" ]; then - echo "COPILOT_GITHUB_TOKEN secret is configured" - fi - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Setup Node.js - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 - with: - node-version: '24' - package-manager-cache: false - - name: Install GitHub Copilot CLI - run: npm install -g @github/copilot@0.0.369 - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "copilot", - engine_name: "GitHub Copilot CLI", - model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", - version: "", - agent_version: "0.0.369", - workflow_name: "Campaign: File Diet", - experimental: false, - supports_tools_allowlist: true, - supports_http_transport: true, - 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, - network_mode: "defaults", - allowed_domains: [], - firewall_enabled: false, - firewall_version: "", - steps: { - firewall: "" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/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)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); - - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require('fs'); - const awInfoPath = '/tmp/gh-aw/aw_info.json'; - - // Load aw_info.json - const awInfo = JSON.parse(fs.readFileSync(awInfoPath, 'utf8')); - - let networkDetails = ''; - if (awInfo.allowed_domains && awInfo.allowed_domains.length > 0) { - networkDetails = awInfo.allowed_domains.slice(0, 10).map(d => ` - ${d}`).join('\n'); - if (awInfo.allowed_domains.length > 10) { - networkDetails += `\n - ... and ${awInfo.allowed_domains.length - 10} more`; - } - } - - const summary = '
\n' + - 'Run details\n\n' + - '#### Engine Configuration\n' + - '| Property | Value |\n' + - '|----------|-------|\n' + - `| Engine ID | ${awInfo.engine_id} |\n` + - `| Engine Name | ${awInfo.engine_name} |\n` + - `| Model | ${awInfo.model || '(default)'} |\n` + - '\n' + - '#### Network Configuration\n' + - '| Property | Value |\n' + - '|----------|-------|\n' + - `| Mode | ${awInfo.network_mode || 'defaults'} |\n` + - `| Firewall | ${awInfo.firewall_enabled ? '✅ Enabled' : '❌ Disabled'} |\n` + - `| Firewall Version | ${awInfo.firewall_version || '(latest)'} |\n` + - '\n' + - (networkDetails ? `##### Allowed Domains\n${networkDetails}\n` : '') + - '
'; - - await core.summary.addRaw(summary).write(); - console.log('Generated workflow overview in step summary'); - - name: Create prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: | - PROMPT_DIR="$(dirname "$GH_AW_PROMPT")" - mkdir -p "$PROMPT_DIR" - cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" - # Campaign Orchestrator - - This workflow orchestrates the 'Campaign: File Diet' campaign. - - - Tracker label: `campaign:code-health-file-diet` - - Associated workflows: daily-file-diet - - Memory paths: memory/campaigns/code-health-file-diet-*/** - - Metrics glob: `memory/campaigns/code-health-file-diet-*/metrics/*.json` - - Use these details to coordinate workers, update metrics, and track progress for this campaign. - - PROMPT_EOF - - name: Append temporary folder instructions to prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: | - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - - /tmp/gh-aw/agent/ - When you need to create temporary files or directories during your work, always use the /tmp/gh-aw/agent/ directory that has been pre-created for you. Do NOT use the root /tmp/ directory directly. - - - PROMPT_EOF - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: | - # Print prompt to workflow logs (equivalent to core.info) - echo "Generated Prompt:" - cat "$GH_AW_PROMPT" - # Print prompt to step summary - { - echo "
" - echo "Generated Prompt" - echo "" - echo '``````markdown' - cat "$GH_AW_PROMPT" - echo '``````' - echo "" - echo "
" - } >> "$GITHUB_STEP_SUMMARY" - - name: Upload prompt - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: prompt.txt - path: /tmp/gh-aw/aw-prompts/prompt.txt - if-no-files-found: warn - - name: Upload agentic run info - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: aw_info.json - path: /tmp/gh-aw/aw_info.json - if-no-files-found: warn - - name: Execute GitHub Copilot CLI - id: agentic_execution - timeout-minutes: 20 - run: | - set -o pipefail - COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" - mkdir -p /tmp/ - mkdir -p /tmp/gh-aw/ - mkdir -p /tmp/gh-aw/agent/ - mkdir -p /tmp/gh-aw/sandbox/agent/logs/ - copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/agent-stdio.log - env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - GITHUB_WORKSPACE: ${{ github.workspace }} - XDG_CONFIG_HOME: /home/runner - - name: Redact secrets in logs - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require("fs"); - const path = require("path"); - function findFiles(dir, extensions) { - const results = []; - try { - if (!fs.existsSync(dir)) { - return results; - } - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - results.push(...findFiles(fullPath, extensions)); - } else if (entry.isFile()) { - const ext = path.extname(entry.name).toLowerCase(); - if (extensions.includes(ext)) { - results.push(fullPath); - } - } - } - } catch (error) { - core.warning(`Failed to scan directory ${dir}: ${error instanceof Error ? error.message : String(error)}`); - } - return results; - } - function redactSecrets(content, secretValues) { - let redactionCount = 0; - let redacted = content; - const sortedSecrets = secretValues.slice().sort((a, b) => b.length - a.length); - for (const secretValue of sortedSecrets) { - if (!secretValue || secretValue.length < 8) { - continue; - } - const prefix = secretValue.substring(0, 3); - const asterisks = "*".repeat(Math.max(0, secretValue.length - 3)); - const replacement = prefix + asterisks; - const parts = redacted.split(secretValue); - const occurrences = parts.length - 1; - if (occurrences > 0) { - redacted = parts.join(replacement); - redactionCount += occurrences; - core.info(`Redacted ${occurrences} occurrence(s) of a secret`); - } - } - return { content: redacted, redactionCount }; - } - function processFile(filePath, secretValues) { - try { - const content = fs.readFileSync(filePath, "utf8"); - const { content: redactedContent, redactionCount } = redactSecrets(content, secretValues); - if (redactionCount > 0) { - fs.writeFileSync(filePath, redactedContent, "utf8"); - core.info(`Processed ${filePath}: ${redactionCount} redaction(s)`); - } - return redactionCount; - } catch (error) { - core.warning(`Failed to process file ${filePath}: ${error instanceof Error ? error.message : String(error)}`); - return 0; - } - } - async function main() { - const secretNames = process.env.GH_AW_SECRET_NAMES; - if (!secretNames) { - core.info("GH_AW_SECRET_NAMES not set, no redaction performed"); - return; - } - core.info("Starting secret redaction in /tmp/gh-aw directory"); - try { - const secretNameList = secretNames.split(",").filter(name => name.trim()); - const secretValues = []; - for (const secretName of secretNameList) { - const envVarName = `SECRET_${secretName}`; - const secretValue = process.env[envVarName]; - if (!secretValue || secretValue.trim() === "") { - continue; - } - secretValues.push(secretValue.trim()); - } - if (secretValues.length === 0) { - core.info("No secret values found to redact"); - return; - } - core.info(`Found ${secretValues.length} secret(s) to redact`); - const targetExtensions = [".txt", ".json", ".log", ".md", ".mdx", ".yml", ".jsonl"]; - const files = findFiles("/tmp/gh-aw", targetExtensions); - core.info(`Found ${files.length} file(s) to scan for secrets`); - let totalRedactions = 0; - let filesWithRedactions = 0; - for (const file of files) { - const redactionCount = processFile(file, secretValues); - if (redactionCount > 0) { - filesWithRedactions++; - totalRedactions += redactionCount; - } - } - if (totalRedactions > 0) { - core.info(`Secret redaction complete: ${totalRedactions} redaction(s) in ${filesWithRedactions} file(s)`); - } else { - core.info("Secret redaction complete: no secrets found"); - } - } catch (error) { - core.setFailed(`Secret redaction failed: ${error instanceof Error ? error.message : String(error)}`); - } - } - await main(); - env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Upload engine output files - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: agent_outputs - path: | - /tmp/gh-aw/sandbox/agent/logs/ - /tmp/gh-aw/redacted-urls.log - if-no-files-found: ignore - - name: Upload MCP logs - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: mcp-logs - path: /tmp/gh-aw/mcp-logs/ - if-no-files-found: ignore - - name: Parse agent logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - with: - script: | - const MAX_TOOL_OUTPUT_LENGTH = 256; - const MAX_STEP_SUMMARY_SIZE = 1000 * 1024; - const MAX_BASH_COMMAND_DISPLAY_LENGTH = 40; - const SIZE_LIMIT_WARNING = "\n\n⚠️ *Step summary size limit reached. Additional content truncated.*\n\n"; - class StepSummaryTracker { - constructor(maxSize = MAX_STEP_SUMMARY_SIZE) { - this.currentSize = 0; - this.maxSize = maxSize; - this.limitReached = false; - } - add(content) { - if (this.limitReached) { - return false; - } - const contentSize = Buffer.byteLength(content, "utf8"); - if (this.currentSize + contentSize > this.maxSize) { - this.limitReached = true; - return false; - } - this.currentSize += contentSize; - return true; - } - isLimitReached() { - return this.limitReached; - } - getSize() { - return this.currentSize; - } - reset() { - this.currentSize = 0; - this.limitReached = false; - } - } - function formatDuration(ms) { - if (!ms || ms <= 0) return ""; - const seconds = Math.round(ms / 1000); - if (seconds < 60) { - return `${seconds}s`; - } - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - if (remainingSeconds === 0) { - return `${minutes}m`; - } - return `${minutes}m ${remainingSeconds}s`; - } - function formatBashCommand(command) { - if (!command) return ""; - let formatted = command - .replace(/\n/g, " ") - .replace(/\r/g, " ") - .replace(/\t/g, " ") - .replace(/\s+/g, " ") - .trim(); - formatted = formatted.replace(/`/g, "\\`"); - const maxLength = 300; - if (formatted.length > maxLength) { - formatted = formatted.substring(0, maxLength) + "..."; - } - return formatted; - } - function truncateString(str, maxLength) { - if (!str) return ""; - if (str.length <= maxLength) return str; - return str.substring(0, maxLength) + "..."; - } - function estimateTokens(text) { - if (!text) return 0; - return Math.ceil(text.length / 4); - } - function formatMcpName(toolName) { - if (toolName.startsWith("mcp__")) { - const parts = toolName.split("__"); - if (parts.length >= 3) { - const provider = parts[1]; - const method = parts.slice(2).join("_"); - return `${provider}::${method}`; - } - } - return toolName; - } - function isLikelyCustomAgent(toolName) { - if (!toolName || typeof toolName !== "string") { - return false; - } - if (!toolName.includes("-")) { - return false; - } - if (toolName.includes("__")) { - return false; - } - if (toolName.toLowerCase().startsWith("safe")) { - return false; - } - if (!/^[a-z0-9]+(-[a-z0-9]+)+$/.test(toolName)) { - return false; - } - return true; - } - function generateConversationMarkdown(logEntries, options) { - const { formatToolCallback, formatInitCallback, summaryTracker } = options; - const toolUsePairs = new Map(); - for (const entry of logEntries) { - if (entry.type === "user" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_result" && content.tool_use_id) { - toolUsePairs.set(content.tool_use_id, content); - } - } - } - } - let markdown = ""; - let sizeLimitReached = false; - function addContent(content) { - if (summaryTracker && !summaryTracker.add(content)) { - sizeLimitReached = true; - return false; - } - markdown += content; - return true; - } - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); - if (initEntry && formatInitCallback) { - if (!addContent("## 🚀 Initialization\n\n")) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - const initResult = formatInitCallback(initEntry); - if (typeof initResult === "string") { - if (!addContent(initResult)) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - } else if (initResult && initResult.markdown) { - if (!addContent(initResult.markdown)) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - } - if (!addContent("\n")) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - } - if (!addContent("\n## 🤖 Reasoning\n\n")) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - for (const entry of logEntries) { - if (sizeLimitReached) break; - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (sizeLimitReached) break; - if (content.type === "text" && content.text) { - const text = content.text.trim(); - if (text && text.length > 0) { - if (!addContent(text + "\n\n")) { - break; - } - } - } else if (content.type === "tool_use") { - const toolResult = toolUsePairs.get(content.id); - const toolMarkdown = formatToolCallback(content, toolResult); - if (toolMarkdown) { - if (!addContent(toolMarkdown)) { - break; - } - } - } - } - } - } - if (sizeLimitReached) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary: [], sizeLimitReached }; - } - if (!addContent("## 🤖 Commands and Tools\n\n")) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary: [], sizeLimitReached: true }; - } - const commandSummary = []; - for (const entry of logEntries) { - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_use") { - const toolName = content.name; - const input = content.input || {}; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - const toolResult = toolUsePairs.get(content.id); - let statusIcon = "❓"; - if (toolResult) { - statusIcon = toolResult.is_error === true ? "❌" : "✅"; - } - if (toolName === "Bash") { - const formattedCommand = formatBashCommand(input.command || ""); - commandSummary.push(`* ${statusIcon} \`${formattedCommand}\``); - } else if (toolName.startsWith("mcp__")) { - const mcpName = formatMcpName(toolName); - commandSummary.push(`* ${statusIcon} \`${mcpName}(...)\``); - } else { - commandSummary.push(`* ${statusIcon} ${toolName}`); - } - } - } - } - } - if (commandSummary.length > 0) { - for (const cmd of commandSummary) { - if (!addContent(`${cmd}\n`)) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary, sizeLimitReached: true }; - } - } - } else { - if (!addContent("No commands or tools used.\n")) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary, sizeLimitReached: true }; - } - } - return { markdown, commandSummary, sizeLimitReached }; - } - function generateInformationSection(lastEntry, options = {}) { - const { additionalInfoCallback } = options; - let markdown = "\n## 📊 Information\n\n"; - if (!lastEntry) { - return markdown; - } - if (lastEntry.num_turns) { - markdown += `**Turns:** ${lastEntry.num_turns}\n\n`; - } - if (lastEntry.duration_ms) { - const durationSec = Math.round(lastEntry.duration_ms / 1000); - const minutes = Math.floor(durationSec / 60); - const seconds = durationSec % 60; - markdown += `**Duration:** ${minutes}m ${seconds}s\n\n`; - } - if (lastEntry.total_cost_usd) { - markdown += `**Total Cost:** $${lastEntry.total_cost_usd.toFixed(4)}\n\n`; - } - if (additionalInfoCallback) { - const additionalInfo = additionalInfoCallback(lastEntry); - if (additionalInfo) { - markdown += additionalInfo; - } - } - if (lastEntry.usage) { - const usage = lastEntry.usage; - if (usage.input_tokens || usage.output_tokens) { - const inputTokens = usage.input_tokens || 0; - const outputTokens = usage.output_tokens || 0; - const cacheCreationTokens = usage.cache_creation_input_tokens || 0; - const cacheReadTokens = usage.cache_read_input_tokens || 0; - const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; - markdown += `**Token Usage:**\n`; - if (totalTokens > 0) markdown += `- Total: ${totalTokens.toLocaleString()}\n`; - if (usage.input_tokens) markdown += `- Input: ${usage.input_tokens.toLocaleString()}\n`; - if (usage.cache_creation_input_tokens) markdown += `- Cache Creation: ${usage.cache_creation_input_tokens.toLocaleString()}\n`; - if (usage.cache_read_input_tokens) markdown += `- Cache Read: ${usage.cache_read_input_tokens.toLocaleString()}\n`; - if (usage.output_tokens) markdown += `- Output: ${usage.output_tokens.toLocaleString()}\n`; - markdown += "\n"; - } - } - if (lastEntry.permission_denials && lastEntry.permission_denials.length > 0) { - markdown += `**Permission Denials:** ${lastEntry.permission_denials.length}\n\n`; - } - return markdown; - } - function formatMcpParameters(input) { - const keys = Object.keys(input); - if (keys.length === 0) return ""; - const paramStrs = []; - for (const key of keys.slice(0, 4)) { - const value = String(input[key] || ""); - paramStrs.push(`${key}: ${truncateString(value, 40)}`); - } - if (keys.length > 4) { - paramStrs.push("..."); - } - return paramStrs.join(", "); - } - function formatInitializationSummary(initEntry, options = {}) { - const { mcpFailureCallback, modelInfoCallback, includeSlashCommands = false } = options; - let markdown = ""; - const mcpFailures = []; - if (initEntry.model) { - markdown += `**Model:** ${initEntry.model}\n\n`; - } - if (modelInfoCallback) { - const modelInfo = modelInfoCallback(initEntry); - if (modelInfo) { - markdown += modelInfo; - } - } - if (initEntry.session_id) { - markdown += `**Session ID:** ${initEntry.session_id}\n\n`; - } - if (initEntry.cwd) { - const cleanCwd = initEntry.cwd.replace(/^\/home\/runner\/work\/[^\/]+\/[^\/]+/, "."); - markdown += `**Working Directory:** ${cleanCwd}\n\n`; - } - if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { - markdown += "**MCP Servers:**\n"; - for (const server of initEntry.mcp_servers) { - const statusIcon = server.status === "connected" ? "✅" : server.status === "failed" ? "❌" : "❓"; - markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; - if (server.status === "failed") { - mcpFailures.push(server.name); - if (mcpFailureCallback) { - const failureDetails = mcpFailureCallback(server); - if (failureDetails) { - markdown += failureDetails; - } - } - } - } - markdown += "\n"; - } - if (initEntry.tools && Array.isArray(initEntry.tools)) { - markdown += "**Available Tools:**\n"; - const categories = { - Core: [], - "File Operations": [], - Builtin: [], - "Safe Outputs": [], - "Safe Inputs": [], - "Git/GitHub": [], - Playwright: [], - Serena: [], - MCP: [], - "Custom Agents": [], - Other: [], - }; - const builtinTools = [ - "bash", - "write_bash", - "read_bash", - "stop_bash", - "list_bash", - "grep", - "glob", - "view", - "create", - "edit", - "store_memory", - "code_review", - "codeql_checker", - "report_progress", - "report_intent", - "gh-advisory-database", - ]; - const internalTools = ["fetch_copilot_cli_documentation"]; - for (const tool of initEntry.tools) { - const toolLower = tool.toLowerCase(); - if (["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes(tool)) { - categories["Core"].push(tool); - } else if (["Read", "Edit", "MultiEdit", "Write", "LS", "Grep", "Glob", "NotebookEdit"].includes(tool)) { - categories["File Operations"].push(tool); - } else if (builtinTools.includes(toolLower) || internalTools.includes(toolLower)) { - categories["Builtin"].push(tool); - } else if (tool.startsWith("safeoutputs-") || tool.startsWith("safe_outputs-")) { - const toolName = tool.replace(/^safeoutputs-|^safe_outputs-/, ""); - categories["Safe Outputs"].push(toolName); - } else if (tool.startsWith("safeinputs-") || tool.startsWith("safe_inputs-")) { - const toolName = tool.replace(/^safeinputs-|^safe_inputs-/, ""); - categories["Safe Inputs"].push(toolName); - } else if (tool.startsWith("mcp__github__")) { - categories["Git/GitHub"].push(formatMcpName(tool)); - } else if (tool.startsWith("mcp__playwright__")) { - categories["Playwright"].push(formatMcpName(tool)); - } else if (tool.startsWith("mcp__serena__")) { - categories["Serena"].push(formatMcpName(tool)); - } else if (tool.startsWith("mcp__") || ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool)) { - categories["MCP"].push(tool.startsWith("mcp__") ? formatMcpName(tool) : tool); - } else if (isLikelyCustomAgent(tool)) { - categories["Custom Agents"].push(tool); - } else { - categories["Other"].push(tool); - } - } - for (const [category, tools] of Object.entries(categories)) { - if (tools.length > 0) { - markdown += `- **${category}:** ${tools.length} tools\n`; - markdown += ` - ${tools.join(", ")}\n`; - } - } - markdown += "\n"; - } - if (includeSlashCommands && initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { - const commandCount = initEntry.slash_commands.length; - markdown += `**Slash Commands:** ${commandCount} available\n`; - if (commandCount <= 10) { - markdown += `- ${initEntry.slash_commands.join(", ")}\n`; - } else { - markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; - } - markdown += "\n"; - } - if (mcpFailures.length > 0) { - return { markdown, mcpFailures }; - } - return { markdown }; - } - function formatToolUse(toolUse, toolResult, options = {}) { - const { includeDetailedParameters = false } = options; - const toolName = toolUse.name; - const input = toolUse.input || {}; - if (toolName === "TodoWrite") { - return ""; - } - function getStatusIcon() { - if (toolResult) { - return toolResult.is_error === true ? "❌" : "✅"; - } - return "❓"; - } - const statusIcon = getStatusIcon(); - let summary = ""; - let details = ""; - if (toolResult && toolResult.content) { - if (typeof toolResult.content === "string") { - details = toolResult.content; - } else if (Array.isArray(toolResult.content)) { - details = toolResult.content.map(c => (typeof c === "string" ? c : c.text || "")).join("\n"); - } - } - const inputText = JSON.stringify(input); - const outputText = details; - const totalTokens = estimateTokens(inputText) + estimateTokens(outputText); - let metadata = ""; - if (toolResult && toolResult.duration_ms) { - metadata += `${formatDuration(toolResult.duration_ms)} `; - } - if (totalTokens > 0) { - metadata += `~${totalTokens}t`; - } - metadata = metadata.trim(); - switch (toolName) { - case "Bash": - const command = input.command || ""; - const description = input.description || ""; - const formattedCommand = formatBashCommand(command); - if (description) { - summary = `${description}: ${formattedCommand}`; - } else { - summary = `${formattedCommand}`; - } - break; - case "Read": - const filePath = input.file_path || input.path || ""; - const relativePath = filePath.replace(/^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, ""); - summary = `Read ${relativePath}`; - break; - case "Write": - case "Edit": - case "MultiEdit": - const writeFilePath = input.file_path || input.path || ""; - const writeRelativePath = writeFilePath.replace(/^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, ""); - summary = `Write ${writeRelativePath}`; - break; - case "Grep": - case "Glob": - const query = input.query || input.pattern || ""; - summary = `Search for ${truncateString(query, 80)}`; - break; - case "LS": - const lsPath = input.path || ""; - const lsRelativePath = lsPath.replace(/^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, ""); - summary = `LS: ${lsRelativePath || lsPath}`; - break; - default: - if (toolName.startsWith("mcp__")) { - const mcpName = formatMcpName(toolName); - const params = formatMcpParameters(input); - summary = `${mcpName}(${params})`; - } else { - const keys = Object.keys(input); - if (keys.length > 0) { - const mainParam = keys.find(k => ["query", "command", "path", "file_path", "content"].includes(k)) || keys[0]; - const value = String(input[mainParam] || ""); - if (value) { - summary = `${toolName}: ${truncateString(value, 100)}`; - } else { - summary = toolName; - } - } else { - summary = toolName; - } - } - } - const sections = []; - if (includeDetailedParameters) { - const inputKeys = Object.keys(input); - if (inputKeys.length > 0) { - sections.push({ - label: "Parameters", - content: JSON.stringify(input, null, 2), - language: "json", - }); - } - } - if (details && details.trim()) { - sections.push({ - label: includeDetailedParameters ? "Response" : "Output", - content: details, - }); - } - return formatToolCallAsDetails({ - summary, - statusIcon, - sections, - metadata: metadata || undefined, - }); - } - function parseLogEntries(logContent) { - let logEntries; - try { - logEntries = JSON.parse(logContent); - if (!Array.isArray(logEntries) || logEntries.length === 0) { - throw new Error("Not a JSON array or empty array"); - } - return logEntries; - } catch (jsonArrayError) { - logEntries = []; - const lines = logContent.split("\n"); - for (const line of lines) { - const trimmedLine = line.trim(); - if (trimmedLine === "") { - continue; - } - if (trimmedLine.startsWith("[{")) { - try { - const arrayEntries = JSON.parse(trimmedLine); - if (Array.isArray(arrayEntries)) { - logEntries.push(...arrayEntries); - continue; - } - } catch (arrayParseError) { - continue; - } - } - if (!trimmedLine.startsWith("{")) { - continue; - } - try { - const jsonEntry = JSON.parse(trimmedLine); - logEntries.push(jsonEntry); - } catch (jsonLineError) { - continue; - } - } - } - if (!Array.isArray(logEntries) || logEntries.length === 0) { - return null; - } - return logEntries; - } - function formatToolCallAsDetails(options) { - const { summary, statusIcon, sections, metadata, maxContentLength = MAX_TOOL_OUTPUT_LENGTH } = options; - let fullSummary = summary; - if (statusIcon && !summary.startsWith(statusIcon)) { - fullSummary = `${statusIcon} ${summary}`; - } - if (metadata) { - fullSummary += ` ${metadata}`; - } - const hasContent = sections && sections.some(s => s.content && s.content.trim()); - if (!hasContent) { - return `${fullSummary}\n\n`; - } - let detailsContent = ""; - for (const section of sections) { - if (!section.content || !section.content.trim()) { - continue; - } - detailsContent += `**${section.label}:**\n\n`; - let content = section.content; - if (content.length > maxContentLength) { - content = content.substring(0, maxContentLength) + "... (truncated)"; - } - if (section.language) { - detailsContent += `\`\`\`\`\`\`${section.language}\n`; - } else { - detailsContent += "``````\n"; - } - detailsContent += content; - detailsContent += "\n``````\n\n"; - } - detailsContent = detailsContent.trimEnd(); - return `
\n${fullSummary}\n\n${detailsContent}\n
\n\n`; - } - function generatePlainTextSummary(logEntries, options = {}) { - const { model, parserName = "Agent" } = options; - const lines = []; - lines.push(`=== ${parserName} Execution Summary ===`); - if (model) { - lines.push(`Model: ${model}`); - } - lines.push(""); - const toolUsePairs = new Map(); - for (const entry of logEntries) { - if (entry.type === "user" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_result" && content.tool_use_id) { - toolUsePairs.set(content.tool_use_id, content); - } - } - } - } - lines.push("Conversation:"); - lines.push(""); - let conversationLineCount = 0; - const MAX_CONVERSATION_LINES = 50; - let conversationTruncated = false; - for (const entry of logEntries) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (content.type === "text" && content.text) { - const text = content.text.trim(); - if (text && text.length > 0) { - const maxTextLength = 500; - let displayText = text; - if (displayText.length > maxTextLength) { - displayText = displayText.substring(0, maxTextLength) + "..."; - } - const textLines = displayText.split("\n"); - for (const line of textLines) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - lines.push(`Agent: ${line}`); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } else if (content.type === "tool_use") { - const toolName = content.name; - const input = content.input || {}; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - const statusIcon = isError ? "✗" : "✓"; - let displayName; - let resultPreview = ""; - if (toolName === "Bash") { - const cmd = formatBashCommand(input.command || ""); - displayName = `$ ${cmd}`; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const resultLines = resultText.split("\n").filter(l => l.trim()); - if (resultLines.length > 0) { - const previewLine = resultLines[0].substring(0, 80); - if (resultLines.length > 1) { - resultPreview = ` └ ${resultLines.length} lines...`; - } else if (previewLine) { - resultPreview = ` └ ${previewLine}`; - } - } - } - } else if (toolName.startsWith("mcp__")) { - const formattedName = formatMcpName(toolName).replace("::", "-"); - displayName = formattedName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } else { - displayName = toolName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } - lines.push(`${statusIcon} ${displayName}`); - conversationLineCount++; - if (resultPreview) { - lines.push(resultPreview); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } - } - } - if (conversationTruncated) { - lines.push("... (conversation truncated)"); - lines.push(""); - } - const lastEntry = logEntries[logEntries.length - 1]; - lines.push("Statistics:"); - if (lastEntry?.num_turns) { - lines.push(` Turns: ${lastEntry.num_turns}`); - } - if (lastEntry?.duration_ms) { - const duration = formatDuration(lastEntry.duration_ms); - if (duration) { - lines.push(` Duration: ${duration}`); - } - } - let toolCounts = { total: 0, success: 0, error: 0 }; - for (const entry of logEntries) { - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_use") { - const toolName = content.name; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - toolCounts.total++; - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - if (isError) { - toolCounts.error++; - } else { - toolCounts.success++; - } - } - } - } - } - if (toolCounts.total > 0) { - lines.push(` Tools: ${toolCounts.success}/${toolCounts.total} succeeded`); - } - if (lastEntry?.usage) { - const usage = lastEntry.usage; - if (usage.input_tokens || usage.output_tokens) { - const inputTokens = usage.input_tokens || 0; - const outputTokens = usage.output_tokens || 0; - const cacheCreationTokens = usage.cache_creation_input_tokens || 0; - const cacheReadTokens = usage.cache_read_input_tokens || 0; - const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; - lines.push( - ` Tokens: ${totalTokens.toLocaleString()} total (${usage.input_tokens.toLocaleString()} in / ${usage.output_tokens.toLocaleString()} out)` - ); - } - } - if (lastEntry?.total_cost_usd) { - lines.push(` Cost: $${lastEntry.total_cost_usd.toFixed(4)}`); - } - return lines.join("\n"); - } - function generateCopilotCliStyleSummary(logEntries, options = {}) { - const { model, parserName = "Agent" } = options; - const lines = []; - const toolUsePairs = new Map(); - for (const entry of logEntries) { - if (entry.type === "user" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_result" && content.tool_use_id) { - toolUsePairs.set(content.tool_use_id, content); - } - } - } - } - lines.push("```"); - lines.push("Conversation:"); - lines.push(""); - let conversationLineCount = 0; - const MAX_CONVERSATION_LINES = 50; - let conversationTruncated = false; - for (const entry of logEntries) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (content.type === "text" && content.text) { - const text = content.text.trim(); - if (text && text.length > 0) { - const maxTextLength = 500; - let displayText = text; - if (displayText.length > maxTextLength) { - displayText = displayText.substring(0, maxTextLength) + "..."; - } - const textLines = displayText.split("\n"); - for (const line of textLines) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - lines.push(`Agent: ${line}`); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } else if (content.type === "tool_use") { - const toolName = content.name; - const input = content.input || {}; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - const statusIcon = isError ? "✗" : "✓"; - let displayName; - let resultPreview = ""; - if (toolName === "Bash") { - const cmd = formatBashCommand(input.command || ""); - displayName = `$ ${cmd}`; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const resultLines = resultText.split("\n").filter(l => l.trim()); - if (resultLines.length > 0) { - const previewLine = resultLines[0].substring(0, 80); - if (resultLines.length > 1) { - resultPreview = ` └ ${resultLines.length} lines...`; - } else if (previewLine) { - resultPreview = ` └ ${previewLine}`; - } - } - } - } else if (toolName.startsWith("mcp__")) { - const formattedName = formatMcpName(toolName).replace("::", "-"); - displayName = formattedName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } else { - displayName = toolName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } - lines.push(`${statusIcon} ${displayName}`); - conversationLineCount++; - if (resultPreview) { - lines.push(resultPreview); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } - } - } - if (conversationTruncated) { - lines.push("... (conversation truncated)"); - lines.push(""); - } - const lastEntry = logEntries[logEntries.length - 1]; - lines.push("Statistics:"); - if (lastEntry?.num_turns) { - lines.push(` Turns: ${lastEntry.num_turns}`); - } - if (lastEntry?.duration_ms) { - const duration = formatDuration(lastEntry.duration_ms); - if (duration) { - lines.push(` Duration: ${duration}`); - } - } - let toolCounts = { total: 0, success: 0, error: 0 }; - for (const entry of logEntries) { - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_use") { - const toolName = content.name; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - toolCounts.total++; - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - if (isError) { - toolCounts.error++; - } else { - toolCounts.success++; - } - } - } - } - } - if (toolCounts.total > 0) { - lines.push(` Tools: ${toolCounts.success}/${toolCounts.total} succeeded`); - } - if (lastEntry?.usage) { - const usage = lastEntry.usage; - if (usage.input_tokens || usage.output_tokens) { - const inputTokens = usage.input_tokens || 0; - const outputTokens = usage.output_tokens || 0; - const cacheCreationTokens = usage.cache_creation_input_tokens || 0; - const cacheReadTokens = usage.cache_read_input_tokens || 0; - const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; - lines.push( - ` Tokens: ${totalTokens.toLocaleString()} total (${usage.input_tokens.toLocaleString()} in / ${usage.output_tokens.toLocaleString()} out)` - ); - } - } - if (lastEntry?.total_cost_usd) { - lines.push(` Cost: $${lastEntry.total_cost_usd.toFixed(4)}`); - } - lines.push("```"); - return lines.join("\n"); - } - function runLogParser(options) { - const fs = require("fs"); - const path = require("path"); - const { parseLog, parserName, supportsDirectories = false } = options; - try { - const logPath = process.env.GH_AW_AGENT_OUTPUT; - if (!logPath) { - core.info("No agent log file specified"); - return; - } - if (!fs.existsSync(logPath)) { - core.info(`Log path not found: ${logPath}`); - return; - } - let content = ""; - const stat = fs.statSync(logPath); - if (stat.isDirectory()) { - if (!supportsDirectories) { - core.info(`Log path is a directory but ${parserName} parser does not support directories: ${logPath}`); - return; - } - const files = fs.readdirSync(logPath); - const logFiles = files.filter(file => file.endsWith(".log") || file.endsWith(".txt")); - if (logFiles.length === 0) { - core.info(`No log files found in directory: ${logPath}`); - return; - } - logFiles.sort(); - for (const file of logFiles) { - const filePath = path.join(logPath, file); - const fileContent = fs.readFileSync(filePath, "utf8"); - if (content.length > 0 && !content.endsWith("\n")) { - content += "\n"; - } - content += fileContent; - } - } else { - content = fs.readFileSync(logPath, "utf8"); - } - const result = parseLog(content); - let markdown = ""; - let mcpFailures = []; - let maxTurnsHit = false; - let logEntries = null; - if (typeof result === "string") { - markdown = result; - } else if (result && typeof result === "object") { - markdown = result.markdown || ""; - mcpFailures = result.mcpFailures || []; - maxTurnsHit = result.maxTurnsHit || false; - logEntries = result.logEntries || null; - } - if (markdown) { - if (logEntries && Array.isArray(logEntries) && logEntries.length > 0) { - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); - const model = initEntry?.model || null; - const plainTextSummary = generatePlainTextSummary(logEntries, { - model, - parserName, - }); - core.info(plainTextSummary); - const copilotCliStyleMarkdown = generateCopilotCliStyleSummary(logEntries, { - model, - parserName, - }); - core.summary.addRaw(copilotCliStyleMarkdown).write(); - } else { - core.info(`${parserName} log parsed successfully`); - core.summary.addRaw(markdown).write(); - } - } else { - core.error(`Failed to parse ${parserName} log`); - } - if (mcpFailures && mcpFailures.length > 0) { - const failedServers = mcpFailures.join(", "); - core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); - } - if (maxTurnsHit) { - core.setFailed(`Agent execution stopped: max-turns limit reached. The agent did not complete its task successfully.`); - } - } catch (error) { - core.setFailed(error instanceof Error ? error : String(error)); - } - } - function main() { - runLogParser({ - parseLog: parseCopilotLog, - parserName: "Copilot", - supportsDirectories: true, - }); - } - function extractPremiumRequestCount(logContent) { - const patterns = [ - /premium\s+requests?\s+consumed:?\s*(\d+)/i, - /(\d+)\s+premium\s+requests?\s+consumed/i, - /consumed\s+(\d+)\s+premium\s+requests?/i, - ]; - for (const pattern of patterns) { - const match = logContent.match(pattern); - if (match && match[1]) { - const count = parseInt(match[1], 10); - if (!isNaN(count) && count > 0) { - return count; - } - } - } - return 1; - } - function parseCopilotLog(logContent) { - try { - let logEntries; - try { - logEntries = JSON.parse(logContent); - if (!Array.isArray(logEntries)) { - throw new Error("Not a JSON array"); - } - } catch (jsonArrayError) { - const debugLogEntries = parseDebugLogFormat(logContent); - if (debugLogEntries && debugLogEntries.length > 0) { - logEntries = debugLogEntries; - } else { - logEntries = parseLogEntries(logContent); - } - } - if (!logEntries || logEntries.length === 0) { - return { markdown: "## Agent Log Summary\n\nLog format not recognized as Copilot JSON array or JSONL.\n", logEntries: [] }; - } - const conversationResult = generateConversationMarkdown(logEntries, { - formatToolCallback: (toolUse, toolResult) => formatToolUse(toolUse, toolResult, { includeDetailedParameters: true }), - formatInitCallback: initEntry => - formatInitializationSummary(initEntry, { - includeSlashCommands: false, - modelInfoCallback: entry => { - if (!entry.model_info) return ""; - const modelInfo = entry.model_info; - let markdown = ""; - if (modelInfo.name) { - markdown += `**Model Name:** ${modelInfo.name}`; - if (modelInfo.vendor) { - markdown += ` (${modelInfo.vendor})`; - } - markdown += "\n\n"; - } - if (modelInfo.billing) { - const billing = modelInfo.billing; - if (billing.is_premium === true) { - markdown += `**Premium Model:** Yes`; - if (billing.multiplier && billing.multiplier !== 1) { - markdown += ` (${billing.multiplier}x cost multiplier)`; - } - markdown += "\n"; - if (billing.restricted_to && Array.isArray(billing.restricted_to) && billing.restricted_to.length > 0) { - markdown += `**Required Plans:** ${billing.restricted_to.join(", ")}\n`; - } - markdown += "\n"; - } else if (billing.is_premium === false) { - markdown += `**Premium Model:** No\n\n`; - } - } - return markdown; - }, - }), - }); - let markdown = conversationResult.markdown; - const lastEntry = logEntries[logEntries.length - 1]; - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); - markdown += generateInformationSection(lastEntry, { - additionalInfoCallback: entry => { - const isPremiumModel = - initEntry && initEntry.model_info && initEntry.model_info.billing && initEntry.model_info.billing.is_premium === true; - if (isPremiumModel) { - const premiumRequestCount = extractPremiumRequestCount(logContent); - return `**Premium Requests Consumed:** ${premiumRequestCount}\n\n`; - } - return ""; - }, - }); - return { markdown, logEntries }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - return { - markdown: `## Agent Log Summary\n\nError parsing Copilot log (tried both JSON array and JSONL formats): ${errorMessage}\n`, - logEntries: [], - }; - } - } - function scanForToolErrors(logContent) { - const toolErrors = new Map(); - const lines = logContent.split("\n"); - const recentToolCalls = []; - const MAX_RECENT_TOOLS = 10; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes('"tool_calls":') && !line.includes('\\"tool_calls\\"')) { - for (let j = i + 1; j < Math.min(i + 30, lines.length); j++) { - const nextLine = lines[j]; - const idMatch = nextLine.match(/"id":\s*"([^"]+)"/); - const nameMatch = nextLine.match(/"name":\s*"([^"]+)"/) && !nextLine.includes('\\"name\\"'); - if (idMatch) { - const toolId = idMatch[1]; - for (let k = j; k < Math.min(j + 10, lines.length); k++) { - const nameLine = lines[k]; - const funcNameMatch = nameLine.match(/"name":\s*"([^"]+)"/); - if (funcNameMatch && !nameLine.includes('\\"name\\"')) { - const toolName = funcNameMatch[1]; - recentToolCalls.unshift({ id: toolId, name: toolName }); - if (recentToolCalls.length > MAX_RECENT_TOOLS) { - recentToolCalls.pop(); - } - break; - } - } - } - } - } - const errorMatch = line.match(/\[ERROR\].*(?:Tool execution failed|Permission denied|Resource not accessible|Error executing tool)/i); - if (errorMatch) { - const toolNameMatch = line.match(/Tool execution failed:\s*([^\s]+)/i); - const toolIdMatch = line.match(/tool_call_id:\s*([^\s]+)/i); - if (toolNameMatch) { - const toolName = toolNameMatch[1]; - toolErrors.set(toolName, true); - const matchingTool = recentToolCalls.find(t => t.name === toolName); - if (matchingTool) { - toolErrors.set(matchingTool.id, true); - } - } else if (toolIdMatch) { - toolErrors.set(toolIdMatch[1], true); - } else if (recentToolCalls.length > 0) { - const lastTool = recentToolCalls[0]; - toolErrors.set(lastTool.id, true); - toolErrors.set(lastTool.name, true); - } - } - } - return toolErrors; - } - function parseDebugLogFormat(logContent) { - const entries = []; - const lines = logContent.split("\n"); - const toolErrors = scanForToolErrors(logContent); - let model = "unknown"; - let sessionId = null; - let modelInfo = null; - let tools = []; - const modelMatch = logContent.match(/Starting Copilot CLI: ([\d.]+)/); - if (modelMatch) { - sessionId = `copilot-${modelMatch[1]}-${Date.now()}`; - } - const gotModelInfoIndex = logContent.indexOf("[DEBUG] Got model info: {"); - if (gotModelInfoIndex !== -1) { - const jsonStart = logContent.indexOf("{", gotModelInfoIndex); - if (jsonStart !== -1) { - let braceCount = 0; - let inString = false; - let escapeNext = false; - let jsonEnd = -1; - for (let i = jsonStart; i < logContent.length; i++) { - const char = logContent[i]; - if (escapeNext) { - escapeNext = false; - continue; - } - if (char === "\\") { - escapeNext = true; - continue; - } - if (char === '"' && !escapeNext) { - inString = !inString; - continue; - } - if (inString) continue; - if (char === "{") { - braceCount++; - } else if (char === "}") { - braceCount--; - if (braceCount === 0) { - jsonEnd = i + 1; - break; - } - } - } - if (jsonEnd !== -1) { - const modelInfoJson = logContent.substring(jsonStart, jsonEnd); - try { - modelInfo = JSON.parse(modelInfoJson); - } catch (e) { - } - } - } - } - const toolsIndex = logContent.indexOf("[DEBUG] Tools:"); - if (toolsIndex !== -1) { - const afterToolsLine = logContent.indexOf("\n", toolsIndex); - let toolsStart = logContent.indexOf("[DEBUG] [", afterToolsLine); - if (toolsStart !== -1) { - toolsStart = logContent.indexOf("[", toolsStart + 7); - } - if (toolsStart !== -1) { - let bracketCount = 0; - let inString = false; - let escapeNext = false; - let toolsEnd = -1; - for (let i = toolsStart; i < logContent.length; i++) { - const char = logContent[i]; - if (escapeNext) { - escapeNext = false; - continue; - } - if (char === "\\") { - escapeNext = true; - continue; - } - if (char === '"' && !escapeNext) { - inString = !inString; - continue; - } - if (inString) continue; - if (char === "[") { - bracketCount++; - } else if (char === "]") { - bracketCount--; - if (bracketCount === 0) { - toolsEnd = i + 1; - break; - } - } - } - if (toolsEnd !== -1) { - let toolsJson = logContent.substring(toolsStart, toolsEnd); - toolsJson = toolsJson.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /gm, ""); - try { - const toolsArray = JSON.parse(toolsJson); - if (Array.isArray(toolsArray)) { - tools = toolsArray - .map(tool => { - if (tool.type === "function" && tool.function && tool.function.name) { - let name = tool.function.name; - if (name.startsWith("github-")) { - name = "mcp__github__" + name.substring(7); - } else if (name.startsWith("safe_outputs-")) { - name = name; - } - return name; - } - return null; - }) - .filter(name => name !== null); - } - } catch (e) { - } - } - } - } - let inDataBlock = false; - let currentJsonLines = []; - let turnCount = 0; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes("[DEBUG] data:")) { - inDataBlock = true; - currentJsonLines = []; - continue; - } - if (inDataBlock) { - const hasTimestamp = line.match(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z /); - if (hasTimestamp) { - const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); - const isJsonContent = /^[{\[}\]"]/.test(cleanLine) || cleanLine.trim().startsWith('"'); - if (!isJsonContent) { - if (currentJsonLines.length > 0) { - try { - const jsonStr = currentJsonLines.join("\n"); - const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - const originalToolName = toolName; - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; - } - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - const hasError = toolErrors.has(toolId) || toolErrors.has(originalToolName); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: hasError ? "Permission denied or tool execution failed" : "", - is_error: hasError, - }); - } - } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { - entries.push({ - type: "user", - message: { content: toolResults }, - }); - } - } - } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, - }; - } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; - } - } - } catch (e) { - } - } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } - } else { - const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); - currentJsonLines.push(cleanLine); - } - } - } - if (inDataBlock && currentJsonLines.length > 0) { - try { - const jsonStr = currentJsonLines.join("\n"); - const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - const originalToolName = toolName; - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; - } - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - const hasError = toolErrors.has(toolId) || toolErrors.has(originalToolName); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: hasError ? "Permission denied or tool execution failed" : "", - is_error: hasError, - }); - } - } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { - entries.push({ - type: "user", - message: { content: toolResults }, - }); - } - } - } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, - }; - } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; - } - } - } catch (e) { - } - } - if (entries.length > 0) { - const initEntry = { - type: "system", - subtype: "init", - session_id: sessionId, - model: model, - tools: tools, - }; - if (modelInfo) { - initEntry.model_info = modelInfo; - } - entries.unshift(initEntry); - if (entries._lastResult) { - entries.push(entries._lastResult); - delete entries._lastResult; - } - } - return entries; - } - main(); - - name: Upload Agent Stdio - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: agent-stdio.log - path: /tmp/gh-aw/agent-stdio.log - if-no-files-found: warn - - name: Validate agent logs for errors - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - GH_AW_ERROR_PATTERNS: "[{\"id\":\"\",\"pattern\":\"::(error)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - error\"},{\"id\":\"\",\"pattern\":\"::(warning)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - warning\"},{\"id\":\"\",\"pattern\":\"::(notice)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - notice\"},{\"id\":\"\",\"pattern\":\"(ERROR|Error):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Generic ERROR messages\"},{\"id\":\"\",\"pattern\":\"(WARNING|Warning):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Generic WARNING messages\"},{\"id\":\"\",\"pattern\":\"(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\s+\\\\[(ERROR)\\\\]\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI timestamped ERROR messages\"},{\"id\":\"\",\"pattern\":\"(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\s+\\\\[(WARN|WARNING)\\\\]\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI timestamped WARNING messages\"},{\"id\":\"\",\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\]\\\\s+(CRITICAL|ERROR):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI bracketed critical/error messages with timestamp\"},{\"id\":\"\",\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\]\\\\s+(WARNING):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI bracketed warning messages with timestamp\"},{\"id\":\"\",\"pattern\":\"✗\\\\s+(.+)\",\"level_group\":0,\"message_group\":1,\"description\":\"Copilot CLI failed command indicator\"},{\"id\":\"\",\"pattern\":\"(?:command not found|not found):\\\\s*(.+)|(.+):\\\\s*(?:command not found|not found)\",\"level_group\":0,\"message_group\":0,\"description\":\"Shell command not found error\"},{\"id\":\"\",\"pattern\":\"Cannot find module\\\\s+['\\\"](.+)['\\\"]\",\"level_group\":0,\"message_group\":1,\"description\":\"Node.js module not found error\"},{\"id\":\"\",\"pattern\":\"Permission denied and could not request permission from user\",\"level_group\":0,\"message_group\":0,\"description\":\"Copilot CLI permission denied warning (user interaction required)\"},{\"id\":\"\",\"pattern\":\"\\\\berror\\\\b.*permission.*denied\",\"level_group\":0,\"message_group\":0,\"description\":\"Permission denied error (requires error context)\"},{\"id\":\"\",\"pattern\":\"\\\\berror\\\\b.*unauthorized\",\"level_group\":0,\"message_group\":0,\"description\":\"Unauthorized access error (requires error context)\"},{\"id\":\"\",\"pattern\":\"\\\\berror\\\\b.*forbidden\",\"level_group\":0,\"message_group\":0,\"description\":\"Forbidden access error (requires error context)\"}]" - with: - script: | - function main() { - const fs = require("fs"); - const path = require("path"); - core.info("Starting validate_errors.cjs script"); - const startTime = Date.now(); - try { - const logPath = process.env.GH_AW_AGENT_OUTPUT; - if (!logPath) { - throw new Error("GH_AW_AGENT_OUTPUT environment variable is required"); - } - core.info(`Log path: ${logPath}`); - if (!fs.existsSync(logPath)) { - core.info(`Log path not found: ${logPath}`); - core.info("No logs to validate - skipping error validation"); - return; - } - const patterns = getErrorPatternsFromEnv(); - if (patterns.length === 0) { - throw new Error("GH_AW_ERROR_PATTERNS environment variable is required and must contain at least one pattern"); - } - core.info(`Loaded ${patterns.length} error patterns`); - core.info(`Patterns: ${JSON.stringify(patterns.map(p => ({ description: p.description, pattern: p.pattern })))}`); - let content = ""; - const stat = fs.statSync(logPath); - if (stat.isDirectory()) { - const files = fs.readdirSync(logPath); - const logFiles = files.filter(file => file.endsWith(".log") || file.endsWith(".txt")); - if (logFiles.length === 0) { - core.info(`No log files found in directory: ${logPath}`); - return; - } - core.info(`Found ${logFiles.length} log files in directory`); - logFiles.sort(); - for (const file of logFiles) { - const filePath = path.join(logPath, file); - const fileContent = fs.readFileSync(filePath, "utf8"); - core.info(`Reading log file: ${file} (${fileContent.length} bytes)`); - content += fileContent; - if (content.length > 0 && !content.endsWith("\n")) { - content += "\n"; - } - } - } else { - content = fs.readFileSync(logPath, "utf8"); - core.info(`Read single log file (${content.length} bytes)`); - } - core.info(`Total log content size: ${content.length} bytes, ${content.split("\n").length} lines`); - const hasErrors = validateErrors(content, patterns); - const elapsedTime = Date.now() - startTime; - core.info(`Error validation completed in ${elapsedTime}ms`); - if (hasErrors) { - core.error("Errors detected in agent logs - continuing workflow step (not failing for now)"); - } else { - core.info("Error validation completed successfully"); - } - } catch (error) { - console.debug(error); - core.error(`Error validating log: ${error instanceof Error ? error.message : String(error)}`); - } - } - function getErrorPatternsFromEnv() { - const patternsEnv = process.env.GH_AW_ERROR_PATTERNS; - if (!patternsEnv) { - throw new Error("GH_AW_ERROR_PATTERNS environment variable is required"); - } - try { - const patterns = JSON.parse(patternsEnv); - if (!Array.isArray(patterns)) { - throw new Error("GH_AW_ERROR_PATTERNS must be a JSON array"); - } - return patterns; - } catch (e) { - throw new Error(`Failed to parse GH_AW_ERROR_PATTERNS as JSON: ${e instanceof Error ? e.message : String(e)}`); - } - } - function shouldSkipLine(line) { - const GITHUB_ACTIONS_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\s+/; - if (new RegExp(GITHUB_ACTIONS_TIMESTAMP.source + "GH_AW_ERROR_PATTERNS:").test(line)) { - return true; - } - if (/^\s+GH_AW_ERROR_PATTERNS:\s*\[/.test(line)) { - return true; - } - if (new RegExp(GITHUB_ACTIONS_TIMESTAMP.source + "env:").test(line)) { - return true; - } - return false; - } - function validateErrors(logContent, patterns) { - const lines = logContent.split("\n"); - let hasErrors = false; - const MAX_ITERATIONS_PER_LINE = 10000; - const ITERATION_WARNING_THRESHOLD = 1000; - const MAX_TOTAL_ERRORS = 100; - const MAX_LINE_LENGTH = 10000; - const TOP_SLOW_PATTERNS_COUNT = 5; - core.info(`Starting error validation with ${patterns.length} patterns and ${lines.length} lines`); - const validationStartTime = Date.now(); - let totalMatches = 0; - let patternStats = []; - for (let patternIndex = 0; patternIndex < patterns.length; patternIndex++) { - const pattern = patterns[patternIndex]; - const patternStartTime = Date.now(); - let patternMatches = 0; - let regex; - try { - regex = new RegExp(pattern.pattern, "g"); - core.info(`Pattern ${patternIndex + 1}/${patterns.length}: ${pattern.description || "Unknown"} - regex: ${pattern.pattern}`); - } catch (e) { - core.error(`invalid error regex pattern: ${pattern.pattern}`); - continue; - } - for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { - const line = lines[lineIndex]; - if (shouldSkipLine(line)) { - continue; - } - if (line.length > MAX_LINE_LENGTH) { - continue; - } - if (totalMatches >= MAX_TOTAL_ERRORS) { - core.warning(`Stopping error validation after finding ${totalMatches} matches (max: ${MAX_TOTAL_ERRORS})`); - break; - } - let match; - let iterationCount = 0; - let lastIndex = -1; - while ((match = regex.exec(line)) !== null) { - iterationCount++; - if (regex.lastIndex === lastIndex) { - core.error(`Infinite loop detected at line ${lineIndex + 1}! Pattern: ${pattern.pattern}, lastIndex stuck at ${lastIndex}`); - core.error(`Line content (truncated): ${truncateString(line, 200)}`); - break; - } - lastIndex = regex.lastIndex; - if (iterationCount === ITERATION_WARNING_THRESHOLD) { - core.warning( - `High iteration count (${iterationCount}) on line ${lineIndex + 1} with pattern: ${pattern.description || pattern.pattern}` - ); - core.warning(`Line content (truncated): ${truncateString(line, 200)}`); - } - if (iterationCount > MAX_ITERATIONS_PER_LINE) { - core.error(`Maximum iteration limit (${MAX_ITERATIONS_PER_LINE}) exceeded at line ${lineIndex + 1}! Pattern: ${pattern.pattern}`); - core.error(`Line content (truncated): ${truncateString(line, 200)}`); - core.error(`This likely indicates a problematic regex pattern. Skipping remaining matches on this line.`); - break; - } - 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); - } - patternMatches++; - totalMatches++; - } - if (iterationCount > 100) { - core.info(`Line ${lineIndex + 1} had ${iterationCount} matches for pattern: ${pattern.description || pattern.pattern}`); - } - } - const patternElapsed = Date.now() - patternStartTime; - patternStats.push({ - description: pattern.description || "Unknown", - pattern: pattern.pattern.substring(0, 50) + (pattern.pattern.length > 50 ? "..." : ""), - matches: patternMatches, - timeMs: patternElapsed, - }); - if (patternElapsed > 5000) { - core.warning(`Pattern "${pattern.description}" took ${patternElapsed}ms to process (${patternMatches} matches)`); - } - if (totalMatches >= MAX_TOTAL_ERRORS) { - core.warning(`Stopping pattern processing after finding ${totalMatches} matches (max: ${MAX_TOTAL_ERRORS})`); - break; - } - } - const validationElapsed = Date.now() - validationStartTime; - core.info(`Validation summary: ${totalMatches} total matches found in ${validationElapsed}ms`); - patternStats.sort((a, b) => b.timeMs - a.timeMs); - const topSlow = patternStats.slice(0, TOP_SLOW_PATTERNS_COUNT); - if (topSlow.length > 0 && topSlow[0].timeMs > 1000) { - core.info(`Top ${TOP_SLOW_PATTERNS_COUNT} slowest patterns:`); - topSlow.forEach((stat, idx) => { - core.info(` ${idx + 1}. "${stat.description}" - ${stat.timeMs}ms (${stat.matches} matches)`); - }); - } - core.info(`Error validation completed. Errors found: ${hasErrors}`); - return hasErrors; - } - function extractLevel(match, pattern) { - if (pattern.level_group && pattern.level_group > 0 && match[pattern.level_group]) { - return match[pattern.level_group]; - } - const fullMatch = match[0]; - if (fullMatch.toLowerCase().includes("error")) { - return "error"; - } else if (fullMatch.toLowerCase().includes("warn")) { - return "warning"; - } - return "unknown"; - } - function extractMessage(match, pattern, fullLine) { - if (pattern.message_group && pattern.message_group > 0 && match[pattern.message_group]) { - return match[pattern.message_group].trim(); - } - return match[0] || fullLine.trim(); - } - function truncateString(str, maxLength) { - if (!str) return ""; - if (str.length <= maxLength) return str; - return str.substring(0, maxLength) + "..."; - } - if (typeof module !== "undefined" && module.exports) { - module.exports = { - validateErrors, - extractLevel, - extractMessage, - getErrorPatternsFromEnv, - truncateString, - shouldSkipLine, - }; - } - if (typeof module === "undefined" || require.main === module) { - main(); - } - - pre_activation: - runs-on: ubuntu-slim - outputs: - activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} - steps: - - name: Check team membership for workflow - id: check_membership - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_REQUIRED_ROLES: - with: - script: | - function parseRequiredPermissions() { - const requiredPermissionsEnv = process.env.GH_AW_REQUIRED_ROLES; - return requiredPermissionsEnv ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") : []; - } - function parseAllowedBots() { - const allowedBotsEnv = process.env.GH_AW_ALLOWED_BOTS; - return allowedBotsEnv ? allowedBotsEnv.split(",").filter(b => b.trim() !== "") : []; - } - async function checkBotStatus(actor, owner, repo) { - try { - const isBot = actor.endsWith("[bot]"); - if (!isBot) { - return { isBot: false, isActive: false }; - } - core.info(`Checking if bot '${actor}' is active on ${owner}/${repo}`); - try { - const botPermission = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - core.info(`Bot '${actor}' is active with permission level: ${botPermission.data.permission}`); - return { isBot: true, isActive: true }; - } catch (botError) { - if (typeof botError === "object" && botError !== null && "status" in botError && botError.status === 404) { - core.warning(`Bot '${actor}' is not active/installed on ${owner}/${repo}`); - return { isBot: true, isActive: false }; - } - const errorMessage = botError instanceof Error ? botError.message : String(botError); - core.warning(`Failed to check bot status: ${errorMessage}`); - return { isBot: true, isActive: false, error: errorMessage }; - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Error checking bot status: ${errorMessage}`); - return { isBot: false, isActive: false, error: errorMessage }; - } - } - async function checkRepositoryPermission(actor, owner, repo, requiredPermissions) { - try { - core.info(`Checking if user '${actor}' has required permissions for ${owner}/${repo}`); - core.info(`Required permissions: ${requiredPermissions.join(", ")}`); - const repoPermission = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - const permission = repoPermission.data.permission; - core.info(`Repository permission level: ${permission}`); - for (const requiredPerm of requiredPermissions) { - if (permission === requiredPerm || (requiredPerm === "maintainer" && permission === "maintain")) { - core.info(`✅ User has ${permission} access to repository`); - return { authorized: true, permission: permission }; - } - } - core.warning(`User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}`); - return { authorized: false, permission: permission }; - } catch (repoError) { - const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); - core.warning(`Repository permission check failed: ${errorMessage}`); - return { authorized: false, error: errorMessage }; - } - } - async function main() { - const { eventName } = context; - const actor = context.actor; - const { owner, repo } = context.repo; - const requiredPermissions = parseRequiredPermissions(); - const allowedBots = parseAllowedBots(); - if (eventName === "workflow_dispatch") { - const hasWriteRole = requiredPermissions.includes("write"); - if (hasWriteRole) { - core.info(`✅ Event ${eventName} does not require validation (write role allowed)`); - core.setOutput("is_team_member", "true"); - core.setOutput("result", "safe_event"); - return; - } - core.info(`Event ${eventName} requires validation (write role not allowed)`); - } - const safeEvents = ["schedule"]; - if (safeEvents.includes(eventName)) { - core.info(`✅ Event ${eventName} does not require validation`); - core.setOutput("is_team_member", "true"); - core.setOutput("result", "safe_event"); - return; - } - if (!requiredPermissions || requiredPermissions.length === 0) { - core.warning("❌ Configuration error: Required permissions not specified. Contact repository administrator."); - core.setOutput("is_team_member", "false"); - core.setOutput("result", "config_error"); - core.setOutput("error_message", "Configuration error: Required permissions not specified"); - return; - } - const result = await checkRepositoryPermission(actor, owner, repo, requiredPermissions); - if (result.error) { - core.setOutput("is_team_member", "false"); - core.setOutput("result", "api_error"); - core.setOutput("error_message", `Repository permission check failed: ${result.error}`); - return; - } - if (result.authorized) { - core.setOutput("is_team_member", "true"); - core.setOutput("result", "authorized"); - core.setOutput("user_permission", result.permission); - } else { - if (allowedBots && allowedBots.length > 0) { - core.info(`Checking if actor '${actor}' is in allowed bots list: ${allowedBots.join(", ")}`); - if (allowedBots.includes(actor)) { - core.info(`Actor '${actor}' is in the allowed bots list`); - const botStatus = await checkBotStatus(actor, owner, repo); - if (botStatus.isBot && botStatus.isActive) { - core.info(`✅ Bot '${actor}' is active on the repository and authorized`); - core.setOutput("is_team_member", "true"); - core.setOutput("result", "authorized_bot"); - core.setOutput("user_permission", "bot"); - return; - } else if (botStatus.isBot && !botStatus.isActive) { - core.warning(`Bot '${actor}' is in the allowed list but not active/installed on ${owner}/${repo}`); - core.setOutput("is_team_member", "false"); - core.setOutput("result", "bot_not_active"); - core.setOutput("user_permission", result.permission); - core.setOutput("error_message", `Access denied: Bot '${actor}' is not active/installed on this repository`); - return; - } else { - core.info(`Actor '${actor}' is in allowed bots list but bot status check failed`); - } - } - } - core.setOutput("is_team_member", "false"); - core.setOutput("result", "insufficient_permissions"); - core.setOutput("user_permission", result.permission); - core.setOutput( - "error_message", - `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - } - } - await main(); - diff --git a/.github/workflows/code-health-file-diet.campaign.g.lock.yml b/.github/workflows/code-health-file-diet.campaign.g.lock.yml index f26dead4958..a0e00a12563 100644 --- a/.github/workflows/code-health-file-diet.campaign.g.lock.yml +++ b/.github/workflows/code-health-file-diet.campaign.g.lock.yml @@ -2270,6 +2270,7 @@ jobs: env: GH_AW_REQUIRED_ROLES: admin,maintainer,write with: + github-token: ${{ secrets.GITHUB_TOKEN }} script: | function parseRequiredPermissions() { const requiredPermissionsEnv = process.env.GH_AW_REQUIRED_ROLES; diff --git a/.github/workflows/daily-malicious-code-scan.lock.yml b/.github/workflows/daily-malicious-code-scan.lock.yml index 3b30268048f..61a9632ef43 100644 --- a/.github/workflows/daily-malicious-code-scan.lock.yml +++ b/.github/workflows/daily-malicious-code-scan.lock.yml @@ -363,8 +363,8 @@ # https://github.com/actions/setup-node/commit/395ad3262231945c25e8478fd5baf05154b1d79f # - actions/upload-artifact@v5 (330a01c490aca151604b8cf639adc76d48f6c5d4) # https://github.com/actions/upload-artifact/commit/330a01c490aca151604b8cf639adc76d48f6c5d4 -# - github/codeql-action/upload-sarif@v3 (6d10af73d5896080e7b530ea0822d927c05661f1) -# https://github.com/github/codeql-action/commit/6d10af73d5896080e7b530ea0822d927c05661f1 +# - github/codeql-action/upload-sarif@v3 (323fb8c0ad5be63b7a6ebf1f32c35882fcfea2cf) +# https://github.com/github/codeql-action/commit/323fb8c0ad5be63b7a6ebf1f32c35882fcfea2cf name: "Daily Malicious Code Scan Agent" "on": @@ -6998,7 +6998,7 @@ jobs: path: ${{ steps.create_code_scanning_alert.outputs.sarif_file }} - name: Upload SARIF to GitHub Security if: steps.create_code_scanning_alert.outputs.sarif_file - uses: github/codeql-action/upload-sarif@6d10af73d5896080e7b530ea0822d927c05661f1 # v3 + uses: github/codeql-action/upload-sarif@323fb8c0ad5be63b7a6ebf1f32c35882fcfea2cf # v3 with: sarif_file: ${{ steps.create_code_scanning_alert.outputs.sarif_file }} diff --git a/.github/workflows/incident-response-campaign.lock.yml b/.github/workflows/incident-response-campaign.lock.yml deleted file mode 100644 index 7cf86d52b6a..00000000000 --- a/.github/workflows/incident-response-campaign.lock.yml +++ /dev/null @@ -1,2403 +0,0 @@ -# -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# 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 -# For more information: https://github.com/githubnext/gh-aw/blob/main/.github/aw/github-agentic-workflows.md -# -# Multi-team incident coordination with command center, SLA tracking, and post-mortem. -# -# Job Dependency Graph: -# ```mermaid -# graph LR -# activation["activation"] -# agent["agent"] -# pre_activation["pre_activation"] -# activation --> agent -# pre_activation --> activation -# ``` -# -# Original Prompt: -# ```markdown -# # Campaign Orchestrator -# -# This workflow orchestrates the 'Campaign: Incident Response' campaign. -# -# - Tracker label: `campaign:incident-response` -# - Associated workflows: incident-response -# - Memory paths: memory/campaigns/incident-*/** -# -# Use these details to coordinate workers, update metrics, and track progress for this campaign. -# ``` -# -# Pinned GitHub Actions: -# - actions/github-script@v8 (ed597411d8f924073f98dfc5c65a23a2325f34cd) -# https://github.com/actions/github-script/commit/ed597411d8f924073f98dfc5c65a23a2325f34cd -# - actions/setup-node@v6 (395ad3262231945c25e8478fd5baf05154b1d79f) -# https://github.com/actions/setup-node/commit/395ad3262231945c25e8478fd5baf05154b1d79f -# - actions/upload-artifact@v5 (330a01c490aca151604b8cf639adc76d48f6c5d4) -# https://github.com/actions/upload-artifact/commit/330a01c490aca151604b8cf639adc76d48f6c5d4 - -name: "Campaign: Incident Response" -on: - workflow_dispatch: - - -permissions: {} - - - - - -jobs: - activation: - needs: pre_activation - if: needs.pre_activation.outputs.activated == 'true' - runs-on: ubuntu-slim - permissions: - contents: read - outputs: - comment_id: "" - comment_repo: "" - steps: - - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_WORKFLOW_FILE: "incident-response-campaign.lock.yml" - with: - script: | - async function main() { - const workflowFile = process.env.GH_AW_WORKFLOW_FILE; - if (!workflowFile) { - core.setFailed("Configuration error: GH_AW_WORKFLOW_FILE not available."); - return; - } - const workflowBasename = workflowFile.replace(".lock.yml", ""); - const workflowMdPath = `.github/workflows/${workflowBasename}.md`; - const lockFilePath = `.github/workflows/${workflowFile}`; - core.info(`Checking workflow timestamps using GitHub API:`); - core.info(` Source: ${workflowMdPath}`); - core.info(` Lock file: ${lockFilePath}`); - const { owner, repo } = context.repo; - const ref = context.sha; - async function getLastCommitForFile(path) { - try { - const response = await github.rest.repos.listCommits({ - owner, - repo, - path, - per_page: 1, - sha: ref, - }); - if (response.data && response.data.length > 0) { - const commit = response.data[0]; - return { - sha: commit.sha, - date: commit.commit.committer.date, - message: commit.commit.message, - }; - } - return null; - } catch (error) { - core.info(`Could not fetch commit for ${path}: ${error.message}`); - return null; - } - } - const workflowCommit = await getLastCommitForFile(workflowMdPath); - const lockCommit = await getLastCommitForFile(lockFilePath); - if (!workflowCommit) { - core.info(`Source file does not exist: ${workflowMdPath}`); - } - if (!lockCommit) { - core.info(`Lock file does not exist: ${lockFilePath}`); - } - if (!workflowCommit || !lockCommit) { - core.info("Skipping timestamp check - one or both files not found"); - return; - } - const workflowDate = new Date(workflowCommit.date); - const lockDate = new Date(lockCommit.date); - core.info(` Source last commit: ${workflowDate.toISOString()} (${workflowCommit.sha.substring(0, 7)})`); - core.info(` Lock last commit: ${lockDate.toISOString()} (${lockCommit.sha.substring(0, 7)})`); - if (workflowDate > lockDate) { - const warningMessage = `WARNING: Lock file '${lockFilePath}' is outdated! The workflow file '${workflowMdPath}' has been modified more recently. Run 'gh aw compile' to regenerate the lock file.`; - core.error(warningMessage); - const workflowTimestamp = workflowDate.toISOString(); - const lockTimestamp = lockDate.toISOString(); - let summary = core.summary - .addRaw("### ⚠️ Workflow Lock File Warning\n\n") - .addRaw("**WARNING**: Lock file is outdated and needs to be regenerated.\n\n") - .addRaw("**Files:**\n") - .addRaw(`- Source: \`${workflowMdPath}\`\n`) - .addRaw(` - Last commit: ${workflowTimestamp}\n`) - .addRaw( - ` - Commit SHA: [\`${workflowCommit.sha.substring(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${workflowCommit.sha})\n` - ) - .addRaw(`- Lock: \`${lockFilePath}\`\n`) - .addRaw(` - Last commit: ${lockTimestamp}\n`) - .addRaw(` - Commit SHA: [\`${lockCommit.sha.substring(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${lockCommit.sha})\n\n`) - .addRaw("**Action Required:** Run `gh aw compile` to regenerate the lock file.\n\n"); - await summary.write(); - } else if (workflowCommit.sha === lockCommit.sha) { - core.info("✅ Lock file is up to date (same commit)"); - } else { - core.info("✅ Lock file is up to date"); - } - } - main().catch(error => { - core.setFailed(error instanceof Error ? error.message : String(error)); - }); - - agent: - needs: activation - outputs: - model: ${{ steps.generate_aw_info.outputs.model }} - steps: - - name: Create gh-aw temp directory - run: | - mkdir -p /tmp/gh-aw/agent - mkdir -p /tmp/gh-aw/sandbox/agent/logs - echo "Created /tmp/gh-aw/agent directory for agentic workflow temporary files" - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Validate COPILOT_GITHUB_TOKEN secret - run: | - if [ -z "$COPILOT_GITHUB_TOKEN" ]; then - { - echo "❌ Error: None of the following secrets are set: COPILOT_GITHUB_TOKEN" - echo "The GitHub Copilot CLI engine requires either COPILOT_GITHUB_TOKEN secret to be configured." - echo "Please configure one of these secrets in your repository settings." - echo "Documentation: https://githubnext.github.io/gh-aw/reference/engines/#github-copilot-default" - } >> "$GITHUB_STEP_SUMMARY" - echo "Error: None of the following secrets are set: COPILOT_GITHUB_TOKEN" - echo "The GitHub Copilot CLI engine requires either COPILOT_GITHUB_TOKEN secret to be configured." - echo "Please configure one of these secrets in your repository settings." - echo "Documentation: https://githubnext.github.io/gh-aw/reference/engines/#github-copilot-default" - exit 1 - fi - - # Log success to stdout (not step summary) - if [ -n "$COPILOT_GITHUB_TOKEN" ]; then - echo "COPILOT_GITHUB_TOKEN secret is configured" - fi - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Setup Node.js - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 - with: - node-version: '24' - package-manager-cache: false - - name: Install GitHub Copilot CLI - run: npm install -g @github/copilot@0.0.369 - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "copilot", - engine_name: "GitHub Copilot CLI", - model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", - version: "", - agent_version: "0.0.369", - workflow_name: "Campaign: Incident Response", - experimental: false, - supports_tools_allowlist: true, - supports_http_transport: true, - 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, - network_mode: "defaults", - allowed_domains: [], - firewall_enabled: false, - firewall_version: "", - steps: { - firewall: "" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/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)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); - - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require('fs'); - const awInfoPath = '/tmp/gh-aw/aw_info.json'; - - // Load aw_info.json - const awInfo = JSON.parse(fs.readFileSync(awInfoPath, 'utf8')); - - let networkDetails = ''; - if (awInfo.allowed_domains && awInfo.allowed_domains.length > 0) { - networkDetails = awInfo.allowed_domains.slice(0, 10).map(d => ` - ${d}`).join('\n'); - if (awInfo.allowed_domains.length > 10) { - networkDetails += `\n - ... and ${awInfo.allowed_domains.length - 10} more`; - } - } - - const summary = '
\n' + - 'Run details\n\n' + - '#### Engine Configuration\n' + - '| Property | Value |\n' + - '|----------|-------|\n' + - `| Engine ID | ${awInfo.engine_id} |\n` + - `| Engine Name | ${awInfo.engine_name} |\n` + - `| Model | ${awInfo.model || '(default)'} |\n` + - '\n' + - '#### Network Configuration\n' + - '| Property | Value |\n' + - '|----------|-------|\n' + - `| Mode | ${awInfo.network_mode || 'defaults'} |\n` + - `| Firewall | ${awInfo.firewall_enabled ? '✅ Enabled' : '❌ Disabled'} |\n` + - `| Firewall Version | ${awInfo.firewall_version || '(latest)'} |\n` + - '\n' + - (networkDetails ? `##### Allowed Domains\n${networkDetails}\n` : '') + - '
'; - - await core.summary.addRaw(summary).write(); - console.log('Generated workflow overview in step summary'); - - name: Create prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: | - PROMPT_DIR="$(dirname "$GH_AW_PROMPT")" - mkdir -p "$PROMPT_DIR" - cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" - # Campaign Orchestrator - - This workflow orchestrates the 'Campaign: Incident Response' campaign. - - - Tracker label: `campaign:incident-response` - - Associated workflows: incident-response - - Memory paths: memory/campaigns/incident-*/** - - Use these details to coordinate workers, update metrics, and track progress for this campaign. - - PROMPT_EOF - - name: Append temporary folder instructions to prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: | - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - - /tmp/gh-aw/agent/ - When you need to create temporary files or directories during your work, always use the /tmp/gh-aw/agent/ directory that has been pre-created for you. Do NOT use the root /tmp/ directory directly. - - - PROMPT_EOF - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: | - # Print prompt to workflow logs (equivalent to core.info) - echo "Generated Prompt:" - cat "$GH_AW_PROMPT" - # Print prompt to step summary - { - echo "
" - echo "Generated Prompt" - echo "" - echo '``````markdown' - cat "$GH_AW_PROMPT" - echo '``````' - echo "" - echo "
" - } >> "$GITHUB_STEP_SUMMARY" - - name: Upload prompt - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: prompt.txt - path: /tmp/gh-aw/aw-prompts/prompt.txt - if-no-files-found: warn - - name: Upload agentic run info - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: aw_info.json - path: /tmp/gh-aw/aw_info.json - if-no-files-found: warn - - name: Execute GitHub Copilot CLI - id: agentic_execution - timeout-minutes: 20 - run: | - set -o pipefail - COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" - mkdir -p /tmp/ - mkdir -p /tmp/gh-aw/ - mkdir -p /tmp/gh-aw/agent/ - mkdir -p /tmp/gh-aw/sandbox/agent/logs/ - copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/agent-stdio.log - env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - GITHUB_WORKSPACE: ${{ github.workspace }} - XDG_CONFIG_HOME: /home/runner - - name: Redact secrets in logs - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require("fs"); - const path = require("path"); - function findFiles(dir, extensions) { - const results = []; - try { - if (!fs.existsSync(dir)) { - return results; - } - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - results.push(...findFiles(fullPath, extensions)); - } else if (entry.isFile()) { - const ext = path.extname(entry.name).toLowerCase(); - if (extensions.includes(ext)) { - results.push(fullPath); - } - } - } - } catch (error) { - core.warning(`Failed to scan directory ${dir}: ${error instanceof Error ? error.message : String(error)}`); - } - return results; - } - function redactSecrets(content, secretValues) { - let redactionCount = 0; - let redacted = content; - const sortedSecrets = secretValues.slice().sort((a, b) => b.length - a.length); - for (const secretValue of sortedSecrets) { - if (!secretValue || secretValue.length < 8) { - continue; - } - const prefix = secretValue.substring(0, 3); - const asterisks = "*".repeat(Math.max(0, secretValue.length - 3)); - const replacement = prefix + asterisks; - const parts = redacted.split(secretValue); - const occurrences = parts.length - 1; - if (occurrences > 0) { - redacted = parts.join(replacement); - redactionCount += occurrences; - core.info(`Redacted ${occurrences} occurrence(s) of a secret`); - } - } - return { content: redacted, redactionCount }; - } - function processFile(filePath, secretValues) { - try { - const content = fs.readFileSync(filePath, "utf8"); - const { content: redactedContent, redactionCount } = redactSecrets(content, secretValues); - if (redactionCount > 0) { - fs.writeFileSync(filePath, redactedContent, "utf8"); - core.info(`Processed ${filePath}: ${redactionCount} redaction(s)`); - } - return redactionCount; - } catch (error) { - core.warning(`Failed to process file ${filePath}: ${error instanceof Error ? error.message : String(error)}`); - return 0; - } - } - async function main() { - const secretNames = process.env.GH_AW_SECRET_NAMES; - if (!secretNames) { - core.info("GH_AW_SECRET_NAMES not set, no redaction performed"); - return; - } - core.info("Starting secret redaction in /tmp/gh-aw directory"); - try { - const secretNameList = secretNames.split(",").filter(name => name.trim()); - const secretValues = []; - for (const secretName of secretNameList) { - const envVarName = `SECRET_${secretName}`; - const secretValue = process.env[envVarName]; - if (!secretValue || secretValue.trim() === "") { - continue; - } - secretValues.push(secretValue.trim()); - } - if (secretValues.length === 0) { - core.info("No secret values found to redact"); - return; - } - core.info(`Found ${secretValues.length} secret(s) to redact`); - const targetExtensions = [".txt", ".json", ".log", ".md", ".mdx", ".yml", ".jsonl"]; - const files = findFiles("/tmp/gh-aw", targetExtensions); - core.info(`Found ${files.length} file(s) to scan for secrets`); - let totalRedactions = 0; - let filesWithRedactions = 0; - for (const file of files) { - const redactionCount = processFile(file, secretValues); - if (redactionCount > 0) { - filesWithRedactions++; - totalRedactions += redactionCount; - } - } - if (totalRedactions > 0) { - core.info(`Secret redaction complete: ${totalRedactions} redaction(s) in ${filesWithRedactions} file(s)`); - } else { - core.info("Secret redaction complete: no secrets found"); - } - } catch (error) { - core.setFailed(`Secret redaction failed: ${error instanceof Error ? error.message : String(error)}`); - } - } - await main(); - env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Upload engine output files - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: agent_outputs - path: | - /tmp/gh-aw/sandbox/agent/logs/ - /tmp/gh-aw/redacted-urls.log - if-no-files-found: ignore - - name: Upload MCP logs - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: mcp-logs - path: /tmp/gh-aw/mcp-logs/ - if-no-files-found: ignore - - name: Parse agent logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - with: - script: | - const MAX_TOOL_OUTPUT_LENGTH = 256; - const MAX_STEP_SUMMARY_SIZE = 1000 * 1024; - const MAX_BASH_COMMAND_DISPLAY_LENGTH = 40; - const SIZE_LIMIT_WARNING = "\n\n⚠️ *Step summary size limit reached. Additional content truncated.*\n\n"; - class StepSummaryTracker { - constructor(maxSize = MAX_STEP_SUMMARY_SIZE) { - this.currentSize = 0; - this.maxSize = maxSize; - this.limitReached = false; - } - add(content) { - if (this.limitReached) { - return false; - } - const contentSize = Buffer.byteLength(content, "utf8"); - if (this.currentSize + contentSize > this.maxSize) { - this.limitReached = true; - return false; - } - this.currentSize += contentSize; - return true; - } - isLimitReached() { - return this.limitReached; - } - getSize() { - return this.currentSize; - } - reset() { - this.currentSize = 0; - this.limitReached = false; - } - } - function formatDuration(ms) { - if (!ms || ms <= 0) return ""; - const seconds = Math.round(ms / 1000); - if (seconds < 60) { - return `${seconds}s`; - } - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - if (remainingSeconds === 0) { - return `${minutes}m`; - } - return `${minutes}m ${remainingSeconds}s`; - } - function formatBashCommand(command) { - if (!command) return ""; - let formatted = command - .replace(/\n/g, " ") - .replace(/\r/g, " ") - .replace(/\t/g, " ") - .replace(/\s+/g, " ") - .trim(); - formatted = formatted.replace(/`/g, "\\`"); - const maxLength = 300; - if (formatted.length > maxLength) { - formatted = formatted.substring(0, maxLength) + "..."; - } - return formatted; - } - function truncateString(str, maxLength) { - if (!str) return ""; - if (str.length <= maxLength) return str; - return str.substring(0, maxLength) + "..."; - } - function estimateTokens(text) { - if (!text) return 0; - return Math.ceil(text.length / 4); - } - function formatMcpName(toolName) { - if (toolName.startsWith("mcp__")) { - const parts = toolName.split("__"); - if (parts.length >= 3) { - const provider = parts[1]; - const method = parts.slice(2).join("_"); - return `${provider}::${method}`; - } - } - return toolName; - } - function isLikelyCustomAgent(toolName) { - if (!toolName || typeof toolName !== "string") { - return false; - } - if (!toolName.includes("-")) { - return false; - } - if (toolName.includes("__")) { - return false; - } - if (toolName.toLowerCase().startsWith("safe")) { - return false; - } - if (!/^[a-z0-9]+(-[a-z0-9]+)+$/.test(toolName)) { - return false; - } - return true; - } - function generateConversationMarkdown(logEntries, options) { - const { formatToolCallback, formatInitCallback, summaryTracker } = options; - const toolUsePairs = new Map(); - for (const entry of logEntries) { - if (entry.type === "user" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_result" && content.tool_use_id) { - toolUsePairs.set(content.tool_use_id, content); - } - } - } - } - let markdown = ""; - let sizeLimitReached = false; - function addContent(content) { - if (summaryTracker && !summaryTracker.add(content)) { - sizeLimitReached = true; - return false; - } - markdown += content; - return true; - } - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); - if (initEntry && formatInitCallback) { - if (!addContent("## 🚀 Initialization\n\n")) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - const initResult = formatInitCallback(initEntry); - if (typeof initResult === "string") { - if (!addContent(initResult)) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - } else if (initResult && initResult.markdown) { - if (!addContent(initResult.markdown)) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - } - if (!addContent("\n")) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - } - if (!addContent("\n## 🤖 Reasoning\n\n")) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - for (const entry of logEntries) { - if (sizeLimitReached) break; - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (sizeLimitReached) break; - if (content.type === "text" && content.text) { - const text = content.text.trim(); - if (text && text.length > 0) { - if (!addContent(text + "\n\n")) { - break; - } - } - } else if (content.type === "tool_use") { - const toolResult = toolUsePairs.get(content.id); - const toolMarkdown = formatToolCallback(content, toolResult); - if (toolMarkdown) { - if (!addContent(toolMarkdown)) { - break; - } - } - } - } - } - } - if (sizeLimitReached) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary: [], sizeLimitReached }; - } - if (!addContent("## 🤖 Commands and Tools\n\n")) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary: [], sizeLimitReached: true }; - } - const commandSummary = []; - for (const entry of logEntries) { - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_use") { - const toolName = content.name; - const input = content.input || {}; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - const toolResult = toolUsePairs.get(content.id); - let statusIcon = "❓"; - if (toolResult) { - statusIcon = toolResult.is_error === true ? "❌" : "✅"; - } - if (toolName === "Bash") { - const formattedCommand = formatBashCommand(input.command || ""); - commandSummary.push(`* ${statusIcon} \`${formattedCommand}\``); - } else if (toolName.startsWith("mcp__")) { - const mcpName = formatMcpName(toolName); - commandSummary.push(`* ${statusIcon} \`${mcpName}(...)\``); - } else { - commandSummary.push(`* ${statusIcon} ${toolName}`); - } - } - } - } - } - if (commandSummary.length > 0) { - for (const cmd of commandSummary) { - if (!addContent(`${cmd}\n`)) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary, sizeLimitReached: true }; - } - } - } else { - if (!addContent("No commands or tools used.\n")) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary, sizeLimitReached: true }; - } - } - return { markdown, commandSummary, sizeLimitReached }; - } - function generateInformationSection(lastEntry, options = {}) { - const { additionalInfoCallback } = options; - let markdown = "\n## 📊 Information\n\n"; - if (!lastEntry) { - return markdown; - } - if (lastEntry.num_turns) { - markdown += `**Turns:** ${lastEntry.num_turns}\n\n`; - } - if (lastEntry.duration_ms) { - const durationSec = Math.round(lastEntry.duration_ms / 1000); - const minutes = Math.floor(durationSec / 60); - const seconds = durationSec % 60; - markdown += `**Duration:** ${minutes}m ${seconds}s\n\n`; - } - if (lastEntry.total_cost_usd) { - markdown += `**Total Cost:** $${lastEntry.total_cost_usd.toFixed(4)}\n\n`; - } - if (additionalInfoCallback) { - const additionalInfo = additionalInfoCallback(lastEntry); - if (additionalInfo) { - markdown += additionalInfo; - } - } - if (lastEntry.usage) { - const usage = lastEntry.usage; - if (usage.input_tokens || usage.output_tokens) { - const inputTokens = usage.input_tokens || 0; - const outputTokens = usage.output_tokens || 0; - const cacheCreationTokens = usage.cache_creation_input_tokens || 0; - const cacheReadTokens = usage.cache_read_input_tokens || 0; - const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; - markdown += `**Token Usage:**\n`; - if (totalTokens > 0) markdown += `- Total: ${totalTokens.toLocaleString()}\n`; - if (usage.input_tokens) markdown += `- Input: ${usage.input_tokens.toLocaleString()}\n`; - if (usage.cache_creation_input_tokens) markdown += `- Cache Creation: ${usage.cache_creation_input_tokens.toLocaleString()}\n`; - if (usage.cache_read_input_tokens) markdown += `- Cache Read: ${usage.cache_read_input_tokens.toLocaleString()}\n`; - if (usage.output_tokens) markdown += `- Output: ${usage.output_tokens.toLocaleString()}\n`; - markdown += "\n"; - } - } - if (lastEntry.permission_denials && lastEntry.permission_denials.length > 0) { - markdown += `**Permission Denials:** ${lastEntry.permission_denials.length}\n\n`; - } - return markdown; - } - function formatMcpParameters(input) { - const keys = Object.keys(input); - if (keys.length === 0) return ""; - const paramStrs = []; - for (const key of keys.slice(0, 4)) { - const value = String(input[key] || ""); - paramStrs.push(`${key}: ${truncateString(value, 40)}`); - } - if (keys.length > 4) { - paramStrs.push("..."); - } - return paramStrs.join(", "); - } - function formatInitializationSummary(initEntry, options = {}) { - const { mcpFailureCallback, modelInfoCallback, includeSlashCommands = false } = options; - let markdown = ""; - const mcpFailures = []; - if (initEntry.model) { - markdown += `**Model:** ${initEntry.model}\n\n`; - } - if (modelInfoCallback) { - const modelInfo = modelInfoCallback(initEntry); - if (modelInfo) { - markdown += modelInfo; - } - } - if (initEntry.session_id) { - markdown += `**Session ID:** ${initEntry.session_id}\n\n`; - } - if (initEntry.cwd) { - const cleanCwd = initEntry.cwd.replace(/^\/home\/runner\/work\/[^\/]+\/[^\/]+/, "."); - markdown += `**Working Directory:** ${cleanCwd}\n\n`; - } - if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { - markdown += "**MCP Servers:**\n"; - for (const server of initEntry.mcp_servers) { - const statusIcon = server.status === "connected" ? "✅" : server.status === "failed" ? "❌" : "❓"; - markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; - if (server.status === "failed") { - mcpFailures.push(server.name); - if (mcpFailureCallback) { - const failureDetails = mcpFailureCallback(server); - if (failureDetails) { - markdown += failureDetails; - } - } - } - } - markdown += "\n"; - } - if (initEntry.tools && Array.isArray(initEntry.tools)) { - markdown += "**Available Tools:**\n"; - const categories = { - Core: [], - "File Operations": [], - Builtin: [], - "Safe Outputs": [], - "Safe Inputs": [], - "Git/GitHub": [], - Playwright: [], - Serena: [], - MCP: [], - "Custom Agents": [], - Other: [], - }; - const builtinTools = [ - "bash", - "write_bash", - "read_bash", - "stop_bash", - "list_bash", - "grep", - "glob", - "view", - "create", - "edit", - "store_memory", - "code_review", - "codeql_checker", - "report_progress", - "report_intent", - "gh-advisory-database", - ]; - const internalTools = ["fetch_copilot_cli_documentation"]; - for (const tool of initEntry.tools) { - const toolLower = tool.toLowerCase(); - if (["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes(tool)) { - categories["Core"].push(tool); - } else if (["Read", "Edit", "MultiEdit", "Write", "LS", "Grep", "Glob", "NotebookEdit"].includes(tool)) { - categories["File Operations"].push(tool); - } else if (builtinTools.includes(toolLower) || internalTools.includes(toolLower)) { - categories["Builtin"].push(tool); - } else if (tool.startsWith("safeoutputs-") || tool.startsWith("safe_outputs-")) { - const toolName = tool.replace(/^safeoutputs-|^safe_outputs-/, ""); - categories["Safe Outputs"].push(toolName); - } else if (tool.startsWith("safeinputs-") || tool.startsWith("safe_inputs-")) { - const toolName = tool.replace(/^safeinputs-|^safe_inputs-/, ""); - categories["Safe Inputs"].push(toolName); - } else if (tool.startsWith("mcp__github__")) { - categories["Git/GitHub"].push(formatMcpName(tool)); - } else if (tool.startsWith("mcp__playwright__")) { - categories["Playwright"].push(formatMcpName(tool)); - } else if (tool.startsWith("mcp__serena__")) { - categories["Serena"].push(formatMcpName(tool)); - } else if (tool.startsWith("mcp__") || ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool)) { - categories["MCP"].push(tool.startsWith("mcp__") ? formatMcpName(tool) : tool); - } else if (isLikelyCustomAgent(tool)) { - categories["Custom Agents"].push(tool); - } else { - categories["Other"].push(tool); - } - } - for (const [category, tools] of Object.entries(categories)) { - if (tools.length > 0) { - markdown += `- **${category}:** ${tools.length} tools\n`; - markdown += ` - ${tools.join(", ")}\n`; - } - } - markdown += "\n"; - } - if (includeSlashCommands && initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { - const commandCount = initEntry.slash_commands.length; - markdown += `**Slash Commands:** ${commandCount} available\n`; - if (commandCount <= 10) { - markdown += `- ${initEntry.slash_commands.join(", ")}\n`; - } else { - markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; - } - markdown += "\n"; - } - if (mcpFailures.length > 0) { - return { markdown, mcpFailures }; - } - return { markdown }; - } - function formatToolUse(toolUse, toolResult, options = {}) { - const { includeDetailedParameters = false } = options; - const toolName = toolUse.name; - const input = toolUse.input || {}; - if (toolName === "TodoWrite") { - return ""; - } - function getStatusIcon() { - if (toolResult) { - return toolResult.is_error === true ? "❌" : "✅"; - } - return "❓"; - } - const statusIcon = getStatusIcon(); - let summary = ""; - let details = ""; - if (toolResult && toolResult.content) { - if (typeof toolResult.content === "string") { - details = toolResult.content; - } else if (Array.isArray(toolResult.content)) { - details = toolResult.content.map(c => (typeof c === "string" ? c : c.text || "")).join("\n"); - } - } - const inputText = JSON.stringify(input); - const outputText = details; - const totalTokens = estimateTokens(inputText) + estimateTokens(outputText); - let metadata = ""; - if (toolResult && toolResult.duration_ms) { - metadata += `${formatDuration(toolResult.duration_ms)} `; - } - if (totalTokens > 0) { - metadata += `~${totalTokens}t`; - } - metadata = metadata.trim(); - switch (toolName) { - case "Bash": - const command = input.command || ""; - const description = input.description || ""; - const formattedCommand = formatBashCommand(command); - if (description) { - summary = `${description}: ${formattedCommand}`; - } else { - summary = `${formattedCommand}`; - } - break; - case "Read": - const filePath = input.file_path || input.path || ""; - const relativePath = filePath.replace(/^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, ""); - summary = `Read ${relativePath}`; - break; - case "Write": - case "Edit": - case "MultiEdit": - const writeFilePath = input.file_path || input.path || ""; - const writeRelativePath = writeFilePath.replace(/^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, ""); - summary = `Write ${writeRelativePath}`; - break; - case "Grep": - case "Glob": - const query = input.query || input.pattern || ""; - summary = `Search for ${truncateString(query, 80)}`; - break; - case "LS": - const lsPath = input.path || ""; - const lsRelativePath = lsPath.replace(/^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, ""); - summary = `LS: ${lsRelativePath || lsPath}`; - break; - default: - if (toolName.startsWith("mcp__")) { - const mcpName = formatMcpName(toolName); - const params = formatMcpParameters(input); - summary = `${mcpName}(${params})`; - } else { - const keys = Object.keys(input); - if (keys.length > 0) { - const mainParam = keys.find(k => ["query", "command", "path", "file_path", "content"].includes(k)) || keys[0]; - const value = String(input[mainParam] || ""); - if (value) { - summary = `${toolName}: ${truncateString(value, 100)}`; - } else { - summary = toolName; - } - } else { - summary = toolName; - } - } - } - const sections = []; - if (includeDetailedParameters) { - const inputKeys = Object.keys(input); - if (inputKeys.length > 0) { - sections.push({ - label: "Parameters", - content: JSON.stringify(input, null, 2), - language: "json", - }); - } - } - if (details && details.trim()) { - sections.push({ - label: includeDetailedParameters ? "Response" : "Output", - content: details, - }); - } - return formatToolCallAsDetails({ - summary, - statusIcon, - sections, - metadata: metadata || undefined, - }); - } - function parseLogEntries(logContent) { - let logEntries; - try { - logEntries = JSON.parse(logContent); - if (!Array.isArray(logEntries) || logEntries.length === 0) { - throw new Error("Not a JSON array or empty array"); - } - return logEntries; - } catch (jsonArrayError) { - logEntries = []; - const lines = logContent.split("\n"); - for (const line of lines) { - const trimmedLine = line.trim(); - if (trimmedLine === "") { - continue; - } - if (trimmedLine.startsWith("[{")) { - try { - const arrayEntries = JSON.parse(trimmedLine); - if (Array.isArray(arrayEntries)) { - logEntries.push(...arrayEntries); - continue; - } - } catch (arrayParseError) { - continue; - } - } - if (!trimmedLine.startsWith("{")) { - continue; - } - try { - const jsonEntry = JSON.parse(trimmedLine); - logEntries.push(jsonEntry); - } catch (jsonLineError) { - continue; - } - } - } - if (!Array.isArray(logEntries) || logEntries.length === 0) { - return null; - } - return logEntries; - } - function formatToolCallAsDetails(options) { - const { summary, statusIcon, sections, metadata, maxContentLength = MAX_TOOL_OUTPUT_LENGTH } = options; - let fullSummary = summary; - if (statusIcon && !summary.startsWith(statusIcon)) { - fullSummary = `${statusIcon} ${summary}`; - } - if (metadata) { - fullSummary += ` ${metadata}`; - } - const hasContent = sections && sections.some(s => s.content && s.content.trim()); - if (!hasContent) { - return `${fullSummary}\n\n`; - } - let detailsContent = ""; - for (const section of sections) { - if (!section.content || !section.content.trim()) { - continue; - } - detailsContent += `**${section.label}:**\n\n`; - let content = section.content; - if (content.length > maxContentLength) { - content = content.substring(0, maxContentLength) + "... (truncated)"; - } - if (section.language) { - detailsContent += `\`\`\`\`\`\`${section.language}\n`; - } else { - detailsContent += "``````\n"; - } - detailsContent += content; - detailsContent += "\n``````\n\n"; - } - detailsContent = detailsContent.trimEnd(); - return `
\n${fullSummary}\n\n${detailsContent}\n
\n\n`; - } - function generatePlainTextSummary(logEntries, options = {}) { - const { model, parserName = "Agent" } = options; - const lines = []; - lines.push(`=== ${parserName} Execution Summary ===`); - if (model) { - lines.push(`Model: ${model}`); - } - lines.push(""); - const toolUsePairs = new Map(); - for (const entry of logEntries) { - if (entry.type === "user" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_result" && content.tool_use_id) { - toolUsePairs.set(content.tool_use_id, content); - } - } - } - } - lines.push("Conversation:"); - lines.push(""); - let conversationLineCount = 0; - const MAX_CONVERSATION_LINES = 50; - let conversationTruncated = false; - for (const entry of logEntries) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (content.type === "text" && content.text) { - const text = content.text.trim(); - if (text && text.length > 0) { - const maxTextLength = 500; - let displayText = text; - if (displayText.length > maxTextLength) { - displayText = displayText.substring(0, maxTextLength) + "..."; - } - const textLines = displayText.split("\n"); - for (const line of textLines) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - lines.push(`Agent: ${line}`); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } else if (content.type === "tool_use") { - const toolName = content.name; - const input = content.input || {}; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - const statusIcon = isError ? "✗" : "✓"; - let displayName; - let resultPreview = ""; - if (toolName === "Bash") { - const cmd = formatBashCommand(input.command || ""); - displayName = `$ ${cmd}`; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const resultLines = resultText.split("\n").filter(l => l.trim()); - if (resultLines.length > 0) { - const previewLine = resultLines[0].substring(0, 80); - if (resultLines.length > 1) { - resultPreview = ` └ ${resultLines.length} lines...`; - } else if (previewLine) { - resultPreview = ` └ ${previewLine}`; - } - } - } - } else if (toolName.startsWith("mcp__")) { - const formattedName = formatMcpName(toolName).replace("::", "-"); - displayName = formattedName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } else { - displayName = toolName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } - lines.push(`${statusIcon} ${displayName}`); - conversationLineCount++; - if (resultPreview) { - lines.push(resultPreview); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } - } - } - if (conversationTruncated) { - lines.push("... (conversation truncated)"); - lines.push(""); - } - const lastEntry = logEntries[logEntries.length - 1]; - lines.push("Statistics:"); - if (lastEntry?.num_turns) { - lines.push(` Turns: ${lastEntry.num_turns}`); - } - if (lastEntry?.duration_ms) { - const duration = formatDuration(lastEntry.duration_ms); - if (duration) { - lines.push(` Duration: ${duration}`); - } - } - let toolCounts = { total: 0, success: 0, error: 0 }; - for (const entry of logEntries) { - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_use") { - const toolName = content.name; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - toolCounts.total++; - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - if (isError) { - toolCounts.error++; - } else { - toolCounts.success++; - } - } - } - } - } - if (toolCounts.total > 0) { - lines.push(` Tools: ${toolCounts.success}/${toolCounts.total} succeeded`); - } - if (lastEntry?.usage) { - const usage = lastEntry.usage; - if (usage.input_tokens || usage.output_tokens) { - const inputTokens = usage.input_tokens || 0; - const outputTokens = usage.output_tokens || 0; - const cacheCreationTokens = usage.cache_creation_input_tokens || 0; - const cacheReadTokens = usage.cache_read_input_tokens || 0; - const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; - lines.push( - ` Tokens: ${totalTokens.toLocaleString()} total (${usage.input_tokens.toLocaleString()} in / ${usage.output_tokens.toLocaleString()} out)` - ); - } - } - if (lastEntry?.total_cost_usd) { - lines.push(` Cost: $${lastEntry.total_cost_usd.toFixed(4)}`); - } - return lines.join("\n"); - } - function generateCopilotCliStyleSummary(logEntries, options = {}) { - const { model, parserName = "Agent" } = options; - const lines = []; - const toolUsePairs = new Map(); - for (const entry of logEntries) { - if (entry.type === "user" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_result" && content.tool_use_id) { - toolUsePairs.set(content.tool_use_id, content); - } - } - } - } - lines.push("```"); - lines.push("Conversation:"); - lines.push(""); - let conversationLineCount = 0; - const MAX_CONVERSATION_LINES = 50; - let conversationTruncated = false; - for (const entry of logEntries) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (content.type === "text" && content.text) { - const text = content.text.trim(); - if (text && text.length > 0) { - const maxTextLength = 500; - let displayText = text; - if (displayText.length > maxTextLength) { - displayText = displayText.substring(0, maxTextLength) + "..."; - } - const textLines = displayText.split("\n"); - for (const line of textLines) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - lines.push(`Agent: ${line}`); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } else if (content.type === "tool_use") { - const toolName = content.name; - const input = content.input || {}; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - const statusIcon = isError ? "✗" : "✓"; - let displayName; - let resultPreview = ""; - if (toolName === "Bash") { - const cmd = formatBashCommand(input.command || ""); - displayName = `$ ${cmd}`; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const resultLines = resultText.split("\n").filter(l => l.trim()); - if (resultLines.length > 0) { - const previewLine = resultLines[0].substring(0, 80); - if (resultLines.length > 1) { - resultPreview = ` └ ${resultLines.length} lines...`; - } else if (previewLine) { - resultPreview = ` └ ${previewLine}`; - } - } - } - } else if (toolName.startsWith("mcp__")) { - const formattedName = formatMcpName(toolName).replace("::", "-"); - displayName = formattedName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } else { - displayName = toolName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } - lines.push(`${statusIcon} ${displayName}`); - conversationLineCount++; - if (resultPreview) { - lines.push(resultPreview); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } - } - } - if (conversationTruncated) { - lines.push("... (conversation truncated)"); - lines.push(""); - } - const lastEntry = logEntries[logEntries.length - 1]; - lines.push("Statistics:"); - if (lastEntry?.num_turns) { - lines.push(` Turns: ${lastEntry.num_turns}`); - } - if (lastEntry?.duration_ms) { - const duration = formatDuration(lastEntry.duration_ms); - if (duration) { - lines.push(` Duration: ${duration}`); - } - } - let toolCounts = { total: 0, success: 0, error: 0 }; - for (const entry of logEntries) { - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_use") { - const toolName = content.name; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - toolCounts.total++; - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - if (isError) { - toolCounts.error++; - } else { - toolCounts.success++; - } - } - } - } - } - if (toolCounts.total > 0) { - lines.push(` Tools: ${toolCounts.success}/${toolCounts.total} succeeded`); - } - if (lastEntry?.usage) { - const usage = lastEntry.usage; - if (usage.input_tokens || usage.output_tokens) { - const inputTokens = usage.input_tokens || 0; - const outputTokens = usage.output_tokens || 0; - const cacheCreationTokens = usage.cache_creation_input_tokens || 0; - const cacheReadTokens = usage.cache_read_input_tokens || 0; - const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; - lines.push( - ` Tokens: ${totalTokens.toLocaleString()} total (${usage.input_tokens.toLocaleString()} in / ${usage.output_tokens.toLocaleString()} out)` - ); - } - } - if (lastEntry?.total_cost_usd) { - lines.push(` Cost: $${lastEntry.total_cost_usd.toFixed(4)}`); - } - lines.push("```"); - return lines.join("\n"); - } - function runLogParser(options) { - const fs = require("fs"); - const path = require("path"); - const { parseLog, parserName, supportsDirectories = false } = options; - try { - const logPath = process.env.GH_AW_AGENT_OUTPUT; - if (!logPath) { - core.info("No agent log file specified"); - return; - } - if (!fs.existsSync(logPath)) { - core.info(`Log path not found: ${logPath}`); - return; - } - let content = ""; - const stat = fs.statSync(logPath); - if (stat.isDirectory()) { - if (!supportsDirectories) { - core.info(`Log path is a directory but ${parserName} parser does not support directories: ${logPath}`); - return; - } - const files = fs.readdirSync(logPath); - const logFiles = files.filter(file => file.endsWith(".log") || file.endsWith(".txt")); - if (logFiles.length === 0) { - core.info(`No log files found in directory: ${logPath}`); - return; - } - logFiles.sort(); - for (const file of logFiles) { - const filePath = path.join(logPath, file); - const fileContent = fs.readFileSync(filePath, "utf8"); - if (content.length > 0 && !content.endsWith("\n")) { - content += "\n"; - } - content += fileContent; - } - } else { - content = fs.readFileSync(logPath, "utf8"); - } - const result = parseLog(content); - let markdown = ""; - let mcpFailures = []; - let maxTurnsHit = false; - let logEntries = null; - if (typeof result === "string") { - markdown = result; - } else if (result && typeof result === "object") { - markdown = result.markdown || ""; - mcpFailures = result.mcpFailures || []; - maxTurnsHit = result.maxTurnsHit || false; - logEntries = result.logEntries || null; - } - if (markdown) { - if (logEntries && Array.isArray(logEntries) && logEntries.length > 0) { - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); - const model = initEntry?.model || null; - const plainTextSummary = generatePlainTextSummary(logEntries, { - model, - parserName, - }); - core.info(plainTextSummary); - const copilotCliStyleMarkdown = generateCopilotCliStyleSummary(logEntries, { - model, - parserName, - }); - core.summary.addRaw(copilotCliStyleMarkdown).write(); - } else { - core.info(`${parserName} log parsed successfully`); - core.summary.addRaw(markdown).write(); - } - } else { - core.error(`Failed to parse ${parserName} log`); - } - if (mcpFailures && mcpFailures.length > 0) { - const failedServers = mcpFailures.join(", "); - core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); - } - if (maxTurnsHit) { - core.setFailed(`Agent execution stopped: max-turns limit reached. The agent did not complete its task successfully.`); - } - } catch (error) { - core.setFailed(error instanceof Error ? error : String(error)); - } - } - function main() { - runLogParser({ - parseLog: parseCopilotLog, - parserName: "Copilot", - supportsDirectories: true, - }); - } - function extractPremiumRequestCount(logContent) { - const patterns = [ - /premium\s+requests?\s+consumed:?\s*(\d+)/i, - /(\d+)\s+premium\s+requests?\s+consumed/i, - /consumed\s+(\d+)\s+premium\s+requests?/i, - ]; - for (const pattern of patterns) { - const match = logContent.match(pattern); - if (match && match[1]) { - const count = parseInt(match[1], 10); - if (!isNaN(count) && count > 0) { - return count; - } - } - } - return 1; - } - function parseCopilotLog(logContent) { - try { - let logEntries; - try { - logEntries = JSON.parse(logContent); - if (!Array.isArray(logEntries)) { - throw new Error("Not a JSON array"); - } - } catch (jsonArrayError) { - const debugLogEntries = parseDebugLogFormat(logContent); - if (debugLogEntries && debugLogEntries.length > 0) { - logEntries = debugLogEntries; - } else { - logEntries = parseLogEntries(logContent); - } - } - if (!logEntries || logEntries.length === 0) { - return { markdown: "## Agent Log Summary\n\nLog format not recognized as Copilot JSON array or JSONL.\n", logEntries: [] }; - } - const conversationResult = generateConversationMarkdown(logEntries, { - formatToolCallback: (toolUse, toolResult) => formatToolUse(toolUse, toolResult, { includeDetailedParameters: true }), - formatInitCallback: initEntry => - formatInitializationSummary(initEntry, { - includeSlashCommands: false, - modelInfoCallback: entry => { - if (!entry.model_info) return ""; - const modelInfo = entry.model_info; - let markdown = ""; - if (modelInfo.name) { - markdown += `**Model Name:** ${modelInfo.name}`; - if (modelInfo.vendor) { - markdown += ` (${modelInfo.vendor})`; - } - markdown += "\n\n"; - } - if (modelInfo.billing) { - const billing = modelInfo.billing; - if (billing.is_premium === true) { - markdown += `**Premium Model:** Yes`; - if (billing.multiplier && billing.multiplier !== 1) { - markdown += ` (${billing.multiplier}x cost multiplier)`; - } - markdown += "\n"; - if (billing.restricted_to && Array.isArray(billing.restricted_to) && billing.restricted_to.length > 0) { - markdown += `**Required Plans:** ${billing.restricted_to.join(", ")}\n`; - } - markdown += "\n"; - } else if (billing.is_premium === false) { - markdown += `**Premium Model:** No\n\n`; - } - } - return markdown; - }, - }), - }); - let markdown = conversationResult.markdown; - const lastEntry = logEntries[logEntries.length - 1]; - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); - markdown += generateInformationSection(lastEntry, { - additionalInfoCallback: entry => { - const isPremiumModel = - initEntry && initEntry.model_info && initEntry.model_info.billing && initEntry.model_info.billing.is_premium === true; - if (isPremiumModel) { - const premiumRequestCount = extractPremiumRequestCount(logContent); - return `**Premium Requests Consumed:** ${premiumRequestCount}\n\n`; - } - return ""; - }, - }); - return { markdown, logEntries }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - return { - markdown: `## Agent Log Summary\n\nError parsing Copilot log (tried both JSON array and JSONL formats): ${errorMessage}\n`, - logEntries: [], - }; - } - } - function scanForToolErrors(logContent) { - const toolErrors = new Map(); - const lines = logContent.split("\n"); - const recentToolCalls = []; - const MAX_RECENT_TOOLS = 10; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes('"tool_calls":') && !line.includes('\\"tool_calls\\"')) { - for (let j = i + 1; j < Math.min(i + 30, lines.length); j++) { - const nextLine = lines[j]; - const idMatch = nextLine.match(/"id":\s*"([^"]+)"/); - const nameMatch = nextLine.match(/"name":\s*"([^"]+)"/) && !nextLine.includes('\\"name\\"'); - if (idMatch) { - const toolId = idMatch[1]; - for (let k = j; k < Math.min(j + 10, lines.length); k++) { - const nameLine = lines[k]; - const funcNameMatch = nameLine.match(/"name":\s*"([^"]+)"/); - if (funcNameMatch && !nameLine.includes('\\"name\\"')) { - const toolName = funcNameMatch[1]; - recentToolCalls.unshift({ id: toolId, name: toolName }); - if (recentToolCalls.length > MAX_RECENT_TOOLS) { - recentToolCalls.pop(); - } - break; - } - } - } - } - } - const errorMatch = line.match(/\[ERROR\].*(?:Tool execution failed|Permission denied|Resource not accessible|Error executing tool)/i); - if (errorMatch) { - const toolNameMatch = line.match(/Tool execution failed:\s*([^\s]+)/i); - const toolIdMatch = line.match(/tool_call_id:\s*([^\s]+)/i); - if (toolNameMatch) { - const toolName = toolNameMatch[1]; - toolErrors.set(toolName, true); - const matchingTool = recentToolCalls.find(t => t.name === toolName); - if (matchingTool) { - toolErrors.set(matchingTool.id, true); - } - } else if (toolIdMatch) { - toolErrors.set(toolIdMatch[1], true); - } else if (recentToolCalls.length > 0) { - const lastTool = recentToolCalls[0]; - toolErrors.set(lastTool.id, true); - toolErrors.set(lastTool.name, true); - } - } - } - return toolErrors; - } - function parseDebugLogFormat(logContent) { - const entries = []; - const lines = logContent.split("\n"); - const toolErrors = scanForToolErrors(logContent); - let model = "unknown"; - let sessionId = null; - let modelInfo = null; - let tools = []; - const modelMatch = logContent.match(/Starting Copilot CLI: ([\d.]+)/); - if (modelMatch) { - sessionId = `copilot-${modelMatch[1]}-${Date.now()}`; - } - const gotModelInfoIndex = logContent.indexOf("[DEBUG] Got model info: {"); - if (gotModelInfoIndex !== -1) { - const jsonStart = logContent.indexOf("{", gotModelInfoIndex); - if (jsonStart !== -1) { - let braceCount = 0; - let inString = false; - let escapeNext = false; - let jsonEnd = -1; - for (let i = jsonStart; i < logContent.length; i++) { - const char = logContent[i]; - if (escapeNext) { - escapeNext = false; - continue; - } - if (char === "\\") { - escapeNext = true; - continue; - } - if (char === '"' && !escapeNext) { - inString = !inString; - continue; - } - if (inString) continue; - if (char === "{") { - braceCount++; - } else if (char === "}") { - braceCount--; - if (braceCount === 0) { - jsonEnd = i + 1; - break; - } - } - } - if (jsonEnd !== -1) { - const modelInfoJson = logContent.substring(jsonStart, jsonEnd); - try { - modelInfo = JSON.parse(modelInfoJson); - } catch (e) { - } - } - } - } - const toolsIndex = logContent.indexOf("[DEBUG] Tools:"); - if (toolsIndex !== -1) { - const afterToolsLine = logContent.indexOf("\n", toolsIndex); - let toolsStart = logContent.indexOf("[DEBUG] [", afterToolsLine); - if (toolsStart !== -1) { - toolsStart = logContent.indexOf("[", toolsStart + 7); - } - if (toolsStart !== -1) { - let bracketCount = 0; - let inString = false; - let escapeNext = false; - let toolsEnd = -1; - for (let i = toolsStart; i < logContent.length; i++) { - const char = logContent[i]; - if (escapeNext) { - escapeNext = false; - continue; - } - if (char === "\\") { - escapeNext = true; - continue; - } - if (char === '"' && !escapeNext) { - inString = !inString; - continue; - } - if (inString) continue; - if (char === "[") { - bracketCount++; - } else if (char === "]") { - bracketCount--; - if (bracketCount === 0) { - toolsEnd = i + 1; - break; - } - } - } - if (toolsEnd !== -1) { - let toolsJson = logContent.substring(toolsStart, toolsEnd); - toolsJson = toolsJson.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /gm, ""); - try { - const toolsArray = JSON.parse(toolsJson); - if (Array.isArray(toolsArray)) { - tools = toolsArray - .map(tool => { - if (tool.type === "function" && tool.function && tool.function.name) { - let name = tool.function.name; - if (name.startsWith("github-")) { - name = "mcp__github__" + name.substring(7); - } else if (name.startsWith("safe_outputs-")) { - name = name; - } - return name; - } - return null; - }) - .filter(name => name !== null); - } - } catch (e) { - } - } - } - } - let inDataBlock = false; - let currentJsonLines = []; - let turnCount = 0; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes("[DEBUG] data:")) { - inDataBlock = true; - currentJsonLines = []; - continue; - } - if (inDataBlock) { - const hasTimestamp = line.match(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z /); - if (hasTimestamp) { - const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); - const isJsonContent = /^[{\[}\]"]/.test(cleanLine) || cleanLine.trim().startsWith('"'); - if (!isJsonContent) { - if (currentJsonLines.length > 0) { - try { - const jsonStr = currentJsonLines.join("\n"); - const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - const originalToolName = toolName; - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; - } - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - const hasError = toolErrors.has(toolId) || toolErrors.has(originalToolName); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: hasError ? "Permission denied or tool execution failed" : "", - is_error: hasError, - }); - } - } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { - entries.push({ - type: "user", - message: { content: toolResults }, - }); - } - } - } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, - }; - } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; - } - } - } catch (e) { - } - } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } - } else { - const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); - currentJsonLines.push(cleanLine); - } - } - } - if (inDataBlock && currentJsonLines.length > 0) { - try { - const jsonStr = currentJsonLines.join("\n"); - const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - const originalToolName = toolName; - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; - } - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - const hasError = toolErrors.has(toolId) || toolErrors.has(originalToolName); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: hasError ? "Permission denied or tool execution failed" : "", - is_error: hasError, - }); - } - } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { - entries.push({ - type: "user", - message: { content: toolResults }, - }); - } - } - } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, - }; - } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; - } - } - } catch (e) { - } - } - if (entries.length > 0) { - const initEntry = { - type: "system", - subtype: "init", - session_id: sessionId, - model: model, - tools: tools, - }; - if (modelInfo) { - initEntry.model_info = modelInfo; - } - entries.unshift(initEntry); - if (entries._lastResult) { - entries.push(entries._lastResult); - delete entries._lastResult; - } - } - return entries; - } - main(); - - name: Upload Agent Stdio - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: agent-stdio.log - path: /tmp/gh-aw/agent-stdio.log - if-no-files-found: warn - - name: Validate agent logs for errors - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - GH_AW_ERROR_PATTERNS: "[{\"id\":\"\",\"pattern\":\"::(error)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - error\"},{\"id\":\"\",\"pattern\":\"::(warning)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - warning\"},{\"id\":\"\",\"pattern\":\"::(notice)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - notice\"},{\"id\":\"\",\"pattern\":\"(ERROR|Error):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Generic ERROR messages\"},{\"id\":\"\",\"pattern\":\"(WARNING|Warning):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Generic WARNING messages\"},{\"id\":\"\",\"pattern\":\"(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\s+\\\\[(ERROR)\\\\]\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI timestamped ERROR messages\"},{\"id\":\"\",\"pattern\":\"(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\s+\\\\[(WARN|WARNING)\\\\]\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI timestamped WARNING messages\"},{\"id\":\"\",\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\]\\\\s+(CRITICAL|ERROR):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI bracketed critical/error messages with timestamp\"},{\"id\":\"\",\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\]\\\\s+(WARNING):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI bracketed warning messages with timestamp\"},{\"id\":\"\",\"pattern\":\"✗\\\\s+(.+)\",\"level_group\":0,\"message_group\":1,\"description\":\"Copilot CLI failed command indicator\"},{\"id\":\"\",\"pattern\":\"(?:command not found|not found):\\\\s*(.+)|(.+):\\\\s*(?:command not found|not found)\",\"level_group\":0,\"message_group\":0,\"description\":\"Shell command not found error\"},{\"id\":\"\",\"pattern\":\"Cannot find module\\\\s+['\\\"](.+)['\\\"]\",\"level_group\":0,\"message_group\":1,\"description\":\"Node.js module not found error\"},{\"id\":\"\",\"pattern\":\"Permission denied and could not request permission from user\",\"level_group\":0,\"message_group\":0,\"description\":\"Copilot CLI permission denied warning (user interaction required)\"},{\"id\":\"\",\"pattern\":\"\\\\berror\\\\b.*permission.*denied\",\"level_group\":0,\"message_group\":0,\"description\":\"Permission denied error (requires error context)\"},{\"id\":\"\",\"pattern\":\"\\\\berror\\\\b.*unauthorized\",\"level_group\":0,\"message_group\":0,\"description\":\"Unauthorized access error (requires error context)\"},{\"id\":\"\",\"pattern\":\"\\\\berror\\\\b.*forbidden\",\"level_group\":0,\"message_group\":0,\"description\":\"Forbidden access error (requires error context)\"}]" - with: - script: | - function main() { - const fs = require("fs"); - const path = require("path"); - core.info("Starting validate_errors.cjs script"); - const startTime = Date.now(); - try { - const logPath = process.env.GH_AW_AGENT_OUTPUT; - if (!logPath) { - throw new Error("GH_AW_AGENT_OUTPUT environment variable is required"); - } - core.info(`Log path: ${logPath}`); - if (!fs.existsSync(logPath)) { - core.info(`Log path not found: ${logPath}`); - core.info("No logs to validate - skipping error validation"); - return; - } - const patterns = getErrorPatternsFromEnv(); - if (patterns.length === 0) { - throw new Error("GH_AW_ERROR_PATTERNS environment variable is required and must contain at least one pattern"); - } - core.info(`Loaded ${patterns.length} error patterns`); - core.info(`Patterns: ${JSON.stringify(patterns.map(p => ({ description: p.description, pattern: p.pattern })))}`); - let content = ""; - const stat = fs.statSync(logPath); - if (stat.isDirectory()) { - const files = fs.readdirSync(logPath); - const logFiles = files.filter(file => file.endsWith(".log") || file.endsWith(".txt")); - if (logFiles.length === 0) { - core.info(`No log files found in directory: ${logPath}`); - return; - } - core.info(`Found ${logFiles.length} log files in directory`); - logFiles.sort(); - for (const file of logFiles) { - const filePath = path.join(logPath, file); - const fileContent = fs.readFileSync(filePath, "utf8"); - core.info(`Reading log file: ${file} (${fileContent.length} bytes)`); - content += fileContent; - if (content.length > 0 && !content.endsWith("\n")) { - content += "\n"; - } - } - } else { - content = fs.readFileSync(logPath, "utf8"); - core.info(`Read single log file (${content.length} bytes)`); - } - core.info(`Total log content size: ${content.length} bytes, ${content.split("\n").length} lines`); - const hasErrors = validateErrors(content, patterns); - const elapsedTime = Date.now() - startTime; - core.info(`Error validation completed in ${elapsedTime}ms`); - if (hasErrors) { - core.error("Errors detected in agent logs - continuing workflow step (not failing for now)"); - } else { - core.info("Error validation completed successfully"); - } - } catch (error) { - console.debug(error); - core.error(`Error validating log: ${error instanceof Error ? error.message : String(error)}`); - } - } - function getErrorPatternsFromEnv() { - const patternsEnv = process.env.GH_AW_ERROR_PATTERNS; - if (!patternsEnv) { - throw new Error("GH_AW_ERROR_PATTERNS environment variable is required"); - } - try { - const patterns = JSON.parse(patternsEnv); - if (!Array.isArray(patterns)) { - throw new Error("GH_AW_ERROR_PATTERNS must be a JSON array"); - } - return patterns; - } catch (e) { - throw new Error(`Failed to parse GH_AW_ERROR_PATTERNS as JSON: ${e instanceof Error ? e.message : String(e)}`); - } - } - function shouldSkipLine(line) { - const GITHUB_ACTIONS_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\s+/; - if (new RegExp(GITHUB_ACTIONS_TIMESTAMP.source + "GH_AW_ERROR_PATTERNS:").test(line)) { - return true; - } - if (/^\s+GH_AW_ERROR_PATTERNS:\s*\[/.test(line)) { - return true; - } - if (new RegExp(GITHUB_ACTIONS_TIMESTAMP.source + "env:").test(line)) { - return true; - } - return false; - } - function validateErrors(logContent, patterns) { - const lines = logContent.split("\n"); - let hasErrors = false; - const MAX_ITERATIONS_PER_LINE = 10000; - const ITERATION_WARNING_THRESHOLD = 1000; - const MAX_TOTAL_ERRORS = 100; - const MAX_LINE_LENGTH = 10000; - const TOP_SLOW_PATTERNS_COUNT = 5; - core.info(`Starting error validation with ${patterns.length} patterns and ${lines.length} lines`); - const validationStartTime = Date.now(); - let totalMatches = 0; - let patternStats = []; - for (let patternIndex = 0; patternIndex < patterns.length; patternIndex++) { - const pattern = patterns[patternIndex]; - const patternStartTime = Date.now(); - let patternMatches = 0; - let regex; - try { - regex = new RegExp(pattern.pattern, "g"); - core.info(`Pattern ${patternIndex + 1}/${patterns.length}: ${pattern.description || "Unknown"} - regex: ${pattern.pattern}`); - } catch (e) { - core.error(`invalid error regex pattern: ${pattern.pattern}`); - continue; - } - for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { - const line = lines[lineIndex]; - if (shouldSkipLine(line)) { - continue; - } - if (line.length > MAX_LINE_LENGTH) { - continue; - } - if (totalMatches >= MAX_TOTAL_ERRORS) { - core.warning(`Stopping error validation after finding ${totalMatches} matches (max: ${MAX_TOTAL_ERRORS})`); - break; - } - let match; - let iterationCount = 0; - let lastIndex = -1; - while ((match = regex.exec(line)) !== null) { - iterationCount++; - if (regex.lastIndex === lastIndex) { - core.error(`Infinite loop detected at line ${lineIndex + 1}! Pattern: ${pattern.pattern}, lastIndex stuck at ${lastIndex}`); - core.error(`Line content (truncated): ${truncateString(line, 200)}`); - break; - } - lastIndex = regex.lastIndex; - if (iterationCount === ITERATION_WARNING_THRESHOLD) { - core.warning( - `High iteration count (${iterationCount}) on line ${lineIndex + 1} with pattern: ${pattern.description || pattern.pattern}` - ); - core.warning(`Line content (truncated): ${truncateString(line, 200)}`); - } - if (iterationCount > MAX_ITERATIONS_PER_LINE) { - core.error(`Maximum iteration limit (${MAX_ITERATIONS_PER_LINE}) exceeded at line ${lineIndex + 1}! Pattern: ${pattern.pattern}`); - core.error(`Line content (truncated): ${truncateString(line, 200)}`); - core.error(`This likely indicates a problematic regex pattern. Skipping remaining matches on this line.`); - break; - } - 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); - } - patternMatches++; - totalMatches++; - } - if (iterationCount > 100) { - core.info(`Line ${lineIndex + 1} had ${iterationCount} matches for pattern: ${pattern.description || pattern.pattern}`); - } - } - const patternElapsed = Date.now() - patternStartTime; - patternStats.push({ - description: pattern.description || "Unknown", - pattern: pattern.pattern.substring(0, 50) + (pattern.pattern.length > 50 ? "..." : ""), - matches: patternMatches, - timeMs: patternElapsed, - }); - if (patternElapsed > 5000) { - core.warning(`Pattern "${pattern.description}" took ${patternElapsed}ms to process (${patternMatches} matches)`); - } - if (totalMatches >= MAX_TOTAL_ERRORS) { - core.warning(`Stopping pattern processing after finding ${totalMatches} matches (max: ${MAX_TOTAL_ERRORS})`); - break; - } - } - const validationElapsed = Date.now() - validationStartTime; - core.info(`Validation summary: ${totalMatches} total matches found in ${validationElapsed}ms`); - patternStats.sort((a, b) => b.timeMs - a.timeMs); - const topSlow = patternStats.slice(0, TOP_SLOW_PATTERNS_COUNT); - if (topSlow.length > 0 && topSlow[0].timeMs > 1000) { - core.info(`Top ${TOP_SLOW_PATTERNS_COUNT} slowest patterns:`); - topSlow.forEach((stat, idx) => { - core.info(` ${idx + 1}. "${stat.description}" - ${stat.timeMs}ms (${stat.matches} matches)`); - }); - } - core.info(`Error validation completed. Errors found: ${hasErrors}`); - return hasErrors; - } - function extractLevel(match, pattern) { - if (pattern.level_group && pattern.level_group > 0 && match[pattern.level_group]) { - return match[pattern.level_group]; - } - const fullMatch = match[0]; - if (fullMatch.toLowerCase().includes("error")) { - return "error"; - } else if (fullMatch.toLowerCase().includes("warn")) { - return "warning"; - } - return "unknown"; - } - function extractMessage(match, pattern, fullLine) { - if (pattern.message_group && pattern.message_group > 0 && match[pattern.message_group]) { - return match[pattern.message_group].trim(); - } - return match[0] || fullLine.trim(); - } - function truncateString(str, maxLength) { - if (!str) return ""; - if (str.length <= maxLength) return str; - return str.substring(0, maxLength) + "..."; - } - if (typeof module !== "undefined" && module.exports) { - module.exports = { - validateErrors, - extractLevel, - extractMessage, - getErrorPatternsFromEnv, - truncateString, - shouldSkipLine, - }; - } - if (typeof module === "undefined" || require.main === module) { - main(); - } - - pre_activation: - runs-on: ubuntu-slim - outputs: - activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} - steps: - - name: Check team membership for workflow - id: check_membership - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_REQUIRED_ROLES: - with: - script: | - function parseRequiredPermissions() { - const requiredPermissionsEnv = process.env.GH_AW_REQUIRED_ROLES; - return requiredPermissionsEnv ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") : []; - } - function parseAllowedBots() { - const allowedBotsEnv = process.env.GH_AW_ALLOWED_BOTS; - return allowedBotsEnv ? allowedBotsEnv.split(",").filter(b => b.trim() !== "") : []; - } - async function checkBotStatus(actor, owner, repo) { - try { - const isBot = actor.endsWith("[bot]"); - if (!isBot) { - return { isBot: false, isActive: false }; - } - core.info(`Checking if bot '${actor}' is active on ${owner}/${repo}`); - try { - const botPermission = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - core.info(`Bot '${actor}' is active with permission level: ${botPermission.data.permission}`); - return { isBot: true, isActive: true }; - } catch (botError) { - if (typeof botError === "object" && botError !== null && "status" in botError && botError.status === 404) { - core.warning(`Bot '${actor}' is not active/installed on ${owner}/${repo}`); - return { isBot: true, isActive: false }; - } - const errorMessage = botError instanceof Error ? botError.message : String(botError); - core.warning(`Failed to check bot status: ${errorMessage}`); - return { isBot: true, isActive: false, error: errorMessage }; - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Error checking bot status: ${errorMessage}`); - return { isBot: false, isActive: false, error: errorMessage }; - } - } - async function checkRepositoryPermission(actor, owner, repo, requiredPermissions) { - try { - core.info(`Checking if user '${actor}' has required permissions for ${owner}/${repo}`); - core.info(`Required permissions: ${requiredPermissions.join(", ")}`); - const repoPermission = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - const permission = repoPermission.data.permission; - core.info(`Repository permission level: ${permission}`); - for (const requiredPerm of requiredPermissions) { - if (permission === requiredPerm || (requiredPerm === "maintainer" && permission === "maintain")) { - core.info(`✅ User has ${permission} access to repository`); - return { authorized: true, permission: permission }; - } - } - core.warning(`User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}`); - return { authorized: false, permission: permission }; - } catch (repoError) { - const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); - core.warning(`Repository permission check failed: ${errorMessage}`); - return { authorized: false, error: errorMessage }; - } - } - async function main() { - const { eventName } = context; - const actor = context.actor; - const { owner, repo } = context.repo; - const requiredPermissions = parseRequiredPermissions(); - const allowedBots = parseAllowedBots(); - if (eventName === "workflow_dispatch") { - const hasWriteRole = requiredPermissions.includes("write"); - if (hasWriteRole) { - core.info(`✅ Event ${eventName} does not require validation (write role allowed)`); - core.setOutput("is_team_member", "true"); - core.setOutput("result", "safe_event"); - return; - } - core.info(`Event ${eventName} requires validation (write role not allowed)`); - } - const safeEvents = ["schedule"]; - if (safeEvents.includes(eventName)) { - core.info(`✅ Event ${eventName} does not require validation`); - core.setOutput("is_team_member", "true"); - core.setOutput("result", "safe_event"); - return; - } - if (!requiredPermissions || requiredPermissions.length === 0) { - core.warning("❌ Configuration error: Required permissions not specified. Contact repository administrator."); - core.setOutput("is_team_member", "false"); - core.setOutput("result", "config_error"); - core.setOutput("error_message", "Configuration error: Required permissions not specified"); - return; - } - const result = await checkRepositoryPermission(actor, owner, repo, requiredPermissions); - if (result.error) { - core.setOutput("is_team_member", "false"); - core.setOutput("result", "api_error"); - core.setOutput("error_message", `Repository permission check failed: ${result.error}`); - return; - } - if (result.authorized) { - core.setOutput("is_team_member", "true"); - core.setOutput("result", "authorized"); - core.setOutput("user_permission", result.permission); - } else { - if (allowedBots && allowedBots.length > 0) { - core.info(`Checking if actor '${actor}' is in allowed bots list: ${allowedBots.join(", ")}`); - if (allowedBots.includes(actor)) { - core.info(`Actor '${actor}' is in the allowed bots list`); - const botStatus = await checkBotStatus(actor, owner, repo); - if (botStatus.isBot && botStatus.isActive) { - core.info(`✅ Bot '${actor}' is active on the repository and authorized`); - core.setOutput("is_team_member", "true"); - core.setOutput("result", "authorized_bot"); - core.setOutput("user_permission", "bot"); - return; - } else if (botStatus.isBot && !botStatus.isActive) { - core.warning(`Bot '${actor}' is in the allowed list but not active/installed on ${owner}/${repo}`); - core.setOutput("is_team_member", "false"); - core.setOutput("result", "bot_not_active"); - core.setOutput("user_permission", result.permission); - core.setOutput("error_message", `Access denied: Bot '${actor}' is not active/installed on this repository`); - return; - } else { - core.info(`Actor '${actor}' is in allowed bots list but bot status check failed`); - } - } - } - core.setOutput("is_team_member", "false"); - core.setOutput("result", "insufficient_permissions"); - core.setOutput("user_permission", result.permission); - core.setOutput( - "error_message", - `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - } - } - await main(); - diff --git a/.github/workflows/incident-response.campaign.g.lock.yml b/.github/workflows/incident-response.campaign.g.lock.yml index a3495a31d91..0959f3833be 100644 --- a/.github/workflows/incident-response.campaign.g.lock.yml +++ b/.github/workflows/incident-response.campaign.g.lock.yml @@ -2268,6 +2268,7 @@ jobs: env: GH_AW_REQUIRED_ROLES: admin,maintainer,write with: + github-token: ${{ secrets.GITHUB_TOKEN }} script: | function parseRequiredPermissions() { const requiredPermissionsEnv = process.env.GH_AW_REQUIRED_ROLES; diff --git a/.github/workflows/org-modernization-campaign.lock.yml b/.github/workflows/org-modernization-campaign.lock.yml deleted file mode 100644 index a71fda3e489..00000000000 --- a/.github/workflows/org-modernization-campaign.lock.yml +++ /dev/null @@ -1,2405 +0,0 @@ -# -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# 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 -# For more information: https://github.com/githubnext/gh-aw/blob/main/.github/aw/github-agentic-workflows.md -# -# Cross-repo modernization with human-in-loop approvals and intelligence reporting. -# -# Job Dependency Graph: -# ```mermaid -# graph LR -# activation["activation"] -# agent["agent"] -# pre_activation["pre_activation"] -# activation --> agent -# pre_activation --> activation -# ``` -# -# Original Prompt: -# ```markdown -# # Campaign Orchestrator -# -# This workflow orchestrates the 'Campaign: Org-wide Modernization' campaign. -# -# - Tracker label: `campaign:org-modernization` -# - Associated workflows: org-wide-rollout, human-ai-collaboration, intelligence -# - Memory paths: memory/campaigns/org-modernization-*/** -# - Metrics glob: `memory/campaigns/org-modernization-*/metrics/*.json` -# -# Use these details to coordinate workers, update metrics, and track progress for this campaign. -# ``` -# -# Pinned GitHub Actions: -# - actions/github-script@v8 (ed597411d8f924073f98dfc5c65a23a2325f34cd) -# https://github.com/actions/github-script/commit/ed597411d8f924073f98dfc5c65a23a2325f34cd -# - actions/setup-node@v6 (395ad3262231945c25e8478fd5baf05154b1d79f) -# https://github.com/actions/setup-node/commit/395ad3262231945c25e8478fd5baf05154b1d79f -# - actions/upload-artifact@v5 (330a01c490aca151604b8cf639adc76d48f6c5d4) -# https://github.com/actions/upload-artifact/commit/330a01c490aca151604b8cf639adc76d48f6c5d4 - -name: "Campaign: Org-wide Modernization" -on: - workflow_dispatch: - - -permissions: {} - - - - - -jobs: - activation: - needs: pre_activation - if: needs.pre_activation.outputs.activated == 'true' - runs-on: ubuntu-slim - permissions: - contents: read - outputs: - comment_id: "" - comment_repo: "" - steps: - - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_WORKFLOW_FILE: "org-modernization-campaign.lock.yml" - with: - script: | - async function main() { - const workflowFile = process.env.GH_AW_WORKFLOW_FILE; - if (!workflowFile) { - core.setFailed("Configuration error: GH_AW_WORKFLOW_FILE not available."); - return; - } - const workflowBasename = workflowFile.replace(".lock.yml", ""); - const workflowMdPath = `.github/workflows/${workflowBasename}.md`; - const lockFilePath = `.github/workflows/${workflowFile}`; - core.info(`Checking workflow timestamps using GitHub API:`); - core.info(` Source: ${workflowMdPath}`); - core.info(` Lock file: ${lockFilePath}`); - const { owner, repo } = context.repo; - const ref = context.sha; - async function getLastCommitForFile(path) { - try { - const response = await github.rest.repos.listCommits({ - owner, - repo, - path, - per_page: 1, - sha: ref, - }); - if (response.data && response.data.length > 0) { - const commit = response.data[0]; - return { - sha: commit.sha, - date: commit.commit.committer.date, - message: commit.commit.message, - }; - } - return null; - } catch (error) { - core.info(`Could not fetch commit for ${path}: ${error.message}`); - return null; - } - } - const workflowCommit = await getLastCommitForFile(workflowMdPath); - const lockCommit = await getLastCommitForFile(lockFilePath); - if (!workflowCommit) { - core.info(`Source file does not exist: ${workflowMdPath}`); - } - if (!lockCommit) { - core.info(`Lock file does not exist: ${lockFilePath}`); - } - if (!workflowCommit || !lockCommit) { - core.info("Skipping timestamp check - one or both files not found"); - return; - } - const workflowDate = new Date(workflowCommit.date); - const lockDate = new Date(lockCommit.date); - core.info(` Source last commit: ${workflowDate.toISOString()} (${workflowCommit.sha.substring(0, 7)})`); - core.info(` Lock last commit: ${lockDate.toISOString()} (${lockCommit.sha.substring(0, 7)})`); - if (workflowDate > lockDate) { - const warningMessage = `WARNING: Lock file '${lockFilePath}' is outdated! The workflow file '${workflowMdPath}' has been modified more recently. Run 'gh aw compile' to regenerate the lock file.`; - core.error(warningMessage); - const workflowTimestamp = workflowDate.toISOString(); - const lockTimestamp = lockDate.toISOString(); - let summary = core.summary - .addRaw("### ⚠️ Workflow Lock File Warning\n\n") - .addRaw("**WARNING**: Lock file is outdated and needs to be regenerated.\n\n") - .addRaw("**Files:**\n") - .addRaw(`- Source: \`${workflowMdPath}\`\n`) - .addRaw(` - Last commit: ${workflowTimestamp}\n`) - .addRaw( - ` - Commit SHA: [\`${workflowCommit.sha.substring(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${workflowCommit.sha})\n` - ) - .addRaw(`- Lock: \`${lockFilePath}\`\n`) - .addRaw(` - Last commit: ${lockTimestamp}\n`) - .addRaw(` - Commit SHA: [\`${lockCommit.sha.substring(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${lockCommit.sha})\n\n`) - .addRaw("**Action Required:** Run `gh aw compile` to regenerate the lock file.\n\n"); - await summary.write(); - } else if (workflowCommit.sha === lockCommit.sha) { - core.info("✅ Lock file is up to date (same commit)"); - } else { - core.info("✅ Lock file is up to date"); - } - } - main().catch(error => { - core.setFailed(error instanceof Error ? error.message : String(error)); - }); - - agent: - needs: activation - outputs: - model: ${{ steps.generate_aw_info.outputs.model }} - steps: - - name: Create gh-aw temp directory - run: | - mkdir -p /tmp/gh-aw/agent - mkdir -p /tmp/gh-aw/sandbox/agent/logs - echo "Created /tmp/gh-aw/agent directory for agentic workflow temporary files" - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Validate COPILOT_GITHUB_TOKEN secret - run: | - if [ -z "$COPILOT_GITHUB_TOKEN" ]; then - { - echo "❌ Error: None of the following secrets are set: COPILOT_GITHUB_TOKEN" - echo "The GitHub Copilot CLI engine requires either COPILOT_GITHUB_TOKEN secret to be configured." - echo "Please configure one of these secrets in your repository settings." - echo "Documentation: https://githubnext.github.io/gh-aw/reference/engines/#github-copilot-default" - } >> "$GITHUB_STEP_SUMMARY" - echo "Error: None of the following secrets are set: COPILOT_GITHUB_TOKEN" - echo "The GitHub Copilot CLI engine requires either COPILOT_GITHUB_TOKEN secret to be configured." - echo "Please configure one of these secrets in your repository settings." - echo "Documentation: https://githubnext.github.io/gh-aw/reference/engines/#github-copilot-default" - exit 1 - fi - - # Log success to stdout (not step summary) - if [ -n "$COPILOT_GITHUB_TOKEN" ]; then - echo "COPILOT_GITHUB_TOKEN secret is configured" - fi - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Setup Node.js - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 - with: - node-version: '24' - package-manager-cache: false - - name: Install GitHub Copilot CLI - run: npm install -g @github/copilot@0.0.369 - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "copilot", - engine_name: "GitHub Copilot CLI", - model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", - version: "", - agent_version: "0.0.369", - workflow_name: "Campaign: Org-wide Modernization", - experimental: false, - supports_tools_allowlist: true, - supports_http_transport: true, - 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, - network_mode: "defaults", - allowed_domains: [], - firewall_enabled: false, - firewall_version: "", - steps: { - firewall: "" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/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)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); - - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require('fs'); - const awInfoPath = '/tmp/gh-aw/aw_info.json'; - - // Load aw_info.json - const awInfo = JSON.parse(fs.readFileSync(awInfoPath, 'utf8')); - - let networkDetails = ''; - if (awInfo.allowed_domains && awInfo.allowed_domains.length > 0) { - networkDetails = awInfo.allowed_domains.slice(0, 10).map(d => ` - ${d}`).join('\n'); - if (awInfo.allowed_domains.length > 10) { - networkDetails += `\n - ... and ${awInfo.allowed_domains.length - 10} more`; - } - } - - const summary = '
\n' + - 'Run details\n\n' + - '#### Engine Configuration\n' + - '| Property | Value |\n' + - '|----------|-------|\n' + - `| Engine ID | ${awInfo.engine_id} |\n` + - `| Engine Name | ${awInfo.engine_name} |\n` + - `| Model | ${awInfo.model || '(default)'} |\n` + - '\n' + - '#### Network Configuration\n' + - '| Property | Value |\n' + - '|----------|-------|\n' + - `| Mode | ${awInfo.network_mode || 'defaults'} |\n` + - `| Firewall | ${awInfo.firewall_enabled ? '✅ Enabled' : '❌ Disabled'} |\n` + - `| Firewall Version | ${awInfo.firewall_version || '(latest)'} |\n` + - '\n' + - (networkDetails ? `##### Allowed Domains\n${networkDetails}\n` : '') + - '
'; - - await core.summary.addRaw(summary).write(); - console.log('Generated workflow overview in step summary'); - - name: Create prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: | - PROMPT_DIR="$(dirname "$GH_AW_PROMPT")" - mkdir -p "$PROMPT_DIR" - cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" - # Campaign Orchestrator - - This workflow orchestrates the 'Campaign: Org-wide Modernization' campaign. - - - Tracker label: `campaign:org-modernization` - - Associated workflows: org-wide-rollout, human-ai-collaboration, intelligence - - Memory paths: memory/campaigns/org-modernization-*/** - - Metrics glob: `memory/campaigns/org-modernization-*/metrics/*.json` - - Use these details to coordinate workers, update metrics, and track progress for this campaign. - - PROMPT_EOF - - name: Append temporary folder instructions to prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: | - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - - /tmp/gh-aw/agent/ - When you need to create temporary files or directories during your work, always use the /tmp/gh-aw/agent/ directory that has been pre-created for you. Do NOT use the root /tmp/ directory directly. - - - PROMPT_EOF - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: | - # Print prompt to workflow logs (equivalent to core.info) - echo "Generated Prompt:" - cat "$GH_AW_PROMPT" - # Print prompt to step summary - { - echo "
" - echo "Generated Prompt" - echo "" - echo '``````markdown' - cat "$GH_AW_PROMPT" - echo '``````' - echo "" - echo "
" - } >> "$GITHUB_STEP_SUMMARY" - - name: Upload prompt - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: prompt.txt - path: /tmp/gh-aw/aw-prompts/prompt.txt - if-no-files-found: warn - - name: Upload agentic run info - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: aw_info.json - path: /tmp/gh-aw/aw_info.json - if-no-files-found: warn - - name: Execute GitHub Copilot CLI - id: agentic_execution - timeout-minutes: 20 - run: | - set -o pipefail - COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" - mkdir -p /tmp/ - mkdir -p /tmp/gh-aw/ - mkdir -p /tmp/gh-aw/agent/ - mkdir -p /tmp/gh-aw/sandbox/agent/logs/ - copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/agent-stdio.log - env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - GITHUB_WORKSPACE: ${{ github.workspace }} - XDG_CONFIG_HOME: /home/runner - - name: Redact secrets in logs - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require("fs"); - const path = require("path"); - function findFiles(dir, extensions) { - const results = []; - try { - if (!fs.existsSync(dir)) { - return results; - } - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - results.push(...findFiles(fullPath, extensions)); - } else if (entry.isFile()) { - const ext = path.extname(entry.name).toLowerCase(); - if (extensions.includes(ext)) { - results.push(fullPath); - } - } - } - } catch (error) { - core.warning(`Failed to scan directory ${dir}: ${error instanceof Error ? error.message : String(error)}`); - } - return results; - } - function redactSecrets(content, secretValues) { - let redactionCount = 0; - let redacted = content; - const sortedSecrets = secretValues.slice().sort((a, b) => b.length - a.length); - for (const secretValue of sortedSecrets) { - if (!secretValue || secretValue.length < 8) { - continue; - } - const prefix = secretValue.substring(0, 3); - const asterisks = "*".repeat(Math.max(0, secretValue.length - 3)); - const replacement = prefix + asterisks; - const parts = redacted.split(secretValue); - const occurrences = parts.length - 1; - if (occurrences > 0) { - redacted = parts.join(replacement); - redactionCount += occurrences; - core.info(`Redacted ${occurrences} occurrence(s) of a secret`); - } - } - return { content: redacted, redactionCount }; - } - function processFile(filePath, secretValues) { - try { - const content = fs.readFileSync(filePath, "utf8"); - const { content: redactedContent, redactionCount } = redactSecrets(content, secretValues); - if (redactionCount > 0) { - fs.writeFileSync(filePath, redactedContent, "utf8"); - core.info(`Processed ${filePath}: ${redactionCount} redaction(s)`); - } - return redactionCount; - } catch (error) { - core.warning(`Failed to process file ${filePath}: ${error instanceof Error ? error.message : String(error)}`); - return 0; - } - } - async function main() { - const secretNames = process.env.GH_AW_SECRET_NAMES; - if (!secretNames) { - core.info("GH_AW_SECRET_NAMES not set, no redaction performed"); - return; - } - core.info("Starting secret redaction in /tmp/gh-aw directory"); - try { - const secretNameList = secretNames.split(",").filter(name => name.trim()); - const secretValues = []; - for (const secretName of secretNameList) { - const envVarName = `SECRET_${secretName}`; - const secretValue = process.env[envVarName]; - if (!secretValue || secretValue.trim() === "") { - continue; - } - secretValues.push(secretValue.trim()); - } - if (secretValues.length === 0) { - core.info("No secret values found to redact"); - return; - } - core.info(`Found ${secretValues.length} secret(s) to redact`); - const targetExtensions = [".txt", ".json", ".log", ".md", ".mdx", ".yml", ".jsonl"]; - const files = findFiles("/tmp/gh-aw", targetExtensions); - core.info(`Found ${files.length} file(s) to scan for secrets`); - let totalRedactions = 0; - let filesWithRedactions = 0; - for (const file of files) { - const redactionCount = processFile(file, secretValues); - if (redactionCount > 0) { - filesWithRedactions++; - totalRedactions += redactionCount; - } - } - if (totalRedactions > 0) { - core.info(`Secret redaction complete: ${totalRedactions} redaction(s) in ${filesWithRedactions} file(s)`); - } else { - core.info("Secret redaction complete: no secrets found"); - } - } catch (error) { - core.setFailed(`Secret redaction failed: ${error instanceof Error ? error.message : String(error)}`); - } - } - await main(); - env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Upload engine output files - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: agent_outputs - path: | - /tmp/gh-aw/sandbox/agent/logs/ - /tmp/gh-aw/redacted-urls.log - if-no-files-found: ignore - - name: Upload MCP logs - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: mcp-logs - path: /tmp/gh-aw/mcp-logs/ - if-no-files-found: ignore - - name: Parse agent logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - with: - script: | - const MAX_TOOL_OUTPUT_LENGTH = 256; - const MAX_STEP_SUMMARY_SIZE = 1000 * 1024; - const MAX_BASH_COMMAND_DISPLAY_LENGTH = 40; - const SIZE_LIMIT_WARNING = "\n\n⚠️ *Step summary size limit reached. Additional content truncated.*\n\n"; - class StepSummaryTracker { - constructor(maxSize = MAX_STEP_SUMMARY_SIZE) { - this.currentSize = 0; - this.maxSize = maxSize; - this.limitReached = false; - } - add(content) { - if (this.limitReached) { - return false; - } - const contentSize = Buffer.byteLength(content, "utf8"); - if (this.currentSize + contentSize > this.maxSize) { - this.limitReached = true; - return false; - } - this.currentSize += contentSize; - return true; - } - isLimitReached() { - return this.limitReached; - } - getSize() { - return this.currentSize; - } - reset() { - this.currentSize = 0; - this.limitReached = false; - } - } - function formatDuration(ms) { - if (!ms || ms <= 0) return ""; - const seconds = Math.round(ms / 1000); - if (seconds < 60) { - return `${seconds}s`; - } - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - if (remainingSeconds === 0) { - return `${minutes}m`; - } - return `${minutes}m ${remainingSeconds}s`; - } - function formatBashCommand(command) { - if (!command) return ""; - let formatted = command - .replace(/\n/g, " ") - .replace(/\r/g, " ") - .replace(/\t/g, " ") - .replace(/\s+/g, " ") - .trim(); - formatted = formatted.replace(/`/g, "\\`"); - const maxLength = 300; - if (formatted.length > maxLength) { - formatted = formatted.substring(0, maxLength) + "..."; - } - return formatted; - } - function truncateString(str, maxLength) { - if (!str) return ""; - if (str.length <= maxLength) return str; - return str.substring(0, maxLength) + "..."; - } - function estimateTokens(text) { - if (!text) return 0; - return Math.ceil(text.length / 4); - } - function formatMcpName(toolName) { - if (toolName.startsWith("mcp__")) { - const parts = toolName.split("__"); - if (parts.length >= 3) { - const provider = parts[1]; - const method = parts.slice(2).join("_"); - return `${provider}::${method}`; - } - } - return toolName; - } - function isLikelyCustomAgent(toolName) { - if (!toolName || typeof toolName !== "string") { - return false; - } - if (!toolName.includes("-")) { - return false; - } - if (toolName.includes("__")) { - return false; - } - if (toolName.toLowerCase().startsWith("safe")) { - return false; - } - if (!/^[a-z0-9]+(-[a-z0-9]+)+$/.test(toolName)) { - return false; - } - return true; - } - function generateConversationMarkdown(logEntries, options) { - const { formatToolCallback, formatInitCallback, summaryTracker } = options; - const toolUsePairs = new Map(); - for (const entry of logEntries) { - if (entry.type === "user" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_result" && content.tool_use_id) { - toolUsePairs.set(content.tool_use_id, content); - } - } - } - } - let markdown = ""; - let sizeLimitReached = false; - function addContent(content) { - if (summaryTracker && !summaryTracker.add(content)) { - sizeLimitReached = true; - return false; - } - markdown += content; - return true; - } - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); - if (initEntry && formatInitCallback) { - if (!addContent("## 🚀 Initialization\n\n")) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - const initResult = formatInitCallback(initEntry); - if (typeof initResult === "string") { - if (!addContent(initResult)) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - } else if (initResult && initResult.markdown) { - if (!addContent(initResult.markdown)) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - } - if (!addContent("\n")) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - } - if (!addContent("\n## 🤖 Reasoning\n\n")) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - for (const entry of logEntries) { - if (sizeLimitReached) break; - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (sizeLimitReached) break; - if (content.type === "text" && content.text) { - const text = content.text.trim(); - if (text && text.length > 0) { - if (!addContent(text + "\n\n")) { - break; - } - } - } else if (content.type === "tool_use") { - const toolResult = toolUsePairs.get(content.id); - const toolMarkdown = formatToolCallback(content, toolResult); - if (toolMarkdown) { - if (!addContent(toolMarkdown)) { - break; - } - } - } - } - } - } - if (sizeLimitReached) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary: [], sizeLimitReached }; - } - if (!addContent("## 🤖 Commands and Tools\n\n")) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary: [], sizeLimitReached: true }; - } - const commandSummary = []; - for (const entry of logEntries) { - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_use") { - const toolName = content.name; - const input = content.input || {}; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - const toolResult = toolUsePairs.get(content.id); - let statusIcon = "❓"; - if (toolResult) { - statusIcon = toolResult.is_error === true ? "❌" : "✅"; - } - if (toolName === "Bash") { - const formattedCommand = formatBashCommand(input.command || ""); - commandSummary.push(`* ${statusIcon} \`${formattedCommand}\``); - } else if (toolName.startsWith("mcp__")) { - const mcpName = formatMcpName(toolName); - commandSummary.push(`* ${statusIcon} \`${mcpName}(...)\``); - } else { - commandSummary.push(`* ${statusIcon} ${toolName}`); - } - } - } - } - } - if (commandSummary.length > 0) { - for (const cmd of commandSummary) { - if (!addContent(`${cmd}\n`)) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary, sizeLimitReached: true }; - } - } - } else { - if (!addContent("No commands or tools used.\n")) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary, sizeLimitReached: true }; - } - } - return { markdown, commandSummary, sizeLimitReached }; - } - function generateInformationSection(lastEntry, options = {}) { - const { additionalInfoCallback } = options; - let markdown = "\n## 📊 Information\n\n"; - if (!lastEntry) { - return markdown; - } - if (lastEntry.num_turns) { - markdown += `**Turns:** ${lastEntry.num_turns}\n\n`; - } - if (lastEntry.duration_ms) { - const durationSec = Math.round(lastEntry.duration_ms / 1000); - const minutes = Math.floor(durationSec / 60); - const seconds = durationSec % 60; - markdown += `**Duration:** ${minutes}m ${seconds}s\n\n`; - } - if (lastEntry.total_cost_usd) { - markdown += `**Total Cost:** $${lastEntry.total_cost_usd.toFixed(4)}\n\n`; - } - if (additionalInfoCallback) { - const additionalInfo = additionalInfoCallback(lastEntry); - if (additionalInfo) { - markdown += additionalInfo; - } - } - if (lastEntry.usage) { - const usage = lastEntry.usage; - if (usage.input_tokens || usage.output_tokens) { - const inputTokens = usage.input_tokens || 0; - const outputTokens = usage.output_tokens || 0; - const cacheCreationTokens = usage.cache_creation_input_tokens || 0; - const cacheReadTokens = usage.cache_read_input_tokens || 0; - const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; - markdown += `**Token Usage:**\n`; - if (totalTokens > 0) markdown += `- Total: ${totalTokens.toLocaleString()}\n`; - if (usage.input_tokens) markdown += `- Input: ${usage.input_tokens.toLocaleString()}\n`; - if (usage.cache_creation_input_tokens) markdown += `- Cache Creation: ${usage.cache_creation_input_tokens.toLocaleString()}\n`; - if (usage.cache_read_input_tokens) markdown += `- Cache Read: ${usage.cache_read_input_tokens.toLocaleString()}\n`; - if (usage.output_tokens) markdown += `- Output: ${usage.output_tokens.toLocaleString()}\n`; - markdown += "\n"; - } - } - if (lastEntry.permission_denials && lastEntry.permission_denials.length > 0) { - markdown += `**Permission Denials:** ${lastEntry.permission_denials.length}\n\n`; - } - return markdown; - } - function formatMcpParameters(input) { - const keys = Object.keys(input); - if (keys.length === 0) return ""; - const paramStrs = []; - for (const key of keys.slice(0, 4)) { - const value = String(input[key] || ""); - paramStrs.push(`${key}: ${truncateString(value, 40)}`); - } - if (keys.length > 4) { - paramStrs.push("..."); - } - return paramStrs.join(", "); - } - function formatInitializationSummary(initEntry, options = {}) { - const { mcpFailureCallback, modelInfoCallback, includeSlashCommands = false } = options; - let markdown = ""; - const mcpFailures = []; - if (initEntry.model) { - markdown += `**Model:** ${initEntry.model}\n\n`; - } - if (modelInfoCallback) { - const modelInfo = modelInfoCallback(initEntry); - if (modelInfo) { - markdown += modelInfo; - } - } - if (initEntry.session_id) { - markdown += `**Session ID:** ${initEntry.session_id}\n\n`; - } - if (initEntry.cwd) { - const cleanCwd = initEntry.cwd.replace(/^\/home\/runner\/work\/[^\/]+\/[^\/]+/, "."); - markdown += `**Working Directory:** ${cleanCwd}\n\n`; - } - if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { - markdown += "**MCP Servers:**\n"; - for (const server of initEntry.mcp_servers) { - const statusIcon = server.status === "connected" ? "✅" : server.status === "failed" ? "❌" : "❓"; - markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; - if (server.status === "failed") { - mcpFailures.push(server.name); - if (mcpFailureCallback) { - const failureDetails = mcpFailureCallback(server); - if (failureDetails) { - markdown += failureDetails; - } - } - } - } - markdown += "\n"; - } - if (initEntry.tools && Array.isArray(initEntry.tools)) { - markdown += "**Available Tools:**\n"; - const categories = { - Core: [], - "File Operations": [], - Builtin: [], - "Safe Outputs": [], - "Safe Inputs": [], - "Git/GitHub": [], - Playwright: [], - Serena: [], - MCP: [], - "Custom Agents": [], - Other: [], - }; - const builtinTools = [ - "bash", - "write_bash", - "read_bash", - "stop_bash", - "list_bash", - "grep", - "glob", - "view", - "create", - "edit", - "store_memory", - "code_review", - "codeql_checker", - "report_progress", - "report_intent", - "gh-advisory-database", - ]; - const internalTools = ["fetch_copilot_cli_documentation"]; - for (const tool of initEntry.tools) { - const toolLower = tool.toLowerCase(); - if (["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes(tool)) { - categories["Core"].push(tool); - } else if (["Read", "Edit", "MultiEdit", "Write", "LS", "Grep", "Glob", "NotebookEdit"].includes(tool)) { - categories["File Operations"].push(tool); - } else if (builtinTools.includes(toolLower) || internalTools.includes(toolLower)) { - categories["Builtin"].push(tool); - } else if (tool.startsWith("safeoutputs-") || tool.startsWith("safe_outputs-")) { - const toolName = tool.replace(/^safeoutputs-|^safe_outputs-/, ""); - categories["Safe Outputs"].push(toolName); - } else if (tool.startsWith("safeinputs-") || tool.startsWith("safe_inputs-")) { - const toolName = tool.replace(/^safeinputs-|^safe_inputs-/, ""); - categories["Safe Inputs"].push(toolName); - } else if (tool.startsWith("mcp__github__")) { - categories["Git/GitHub"].push(formatMcpName(tool)); - } else if (tool.startsWith("mcp__playwright__")) { - categories["Playwright"].push(formatMcpName(tool)); - } else if (tool.startsWith("mcp__serena__")) { - categories["Serena"].push(formatMcpName(tool)); - } else if (tool.startsWith("mcp__") || ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool)) { - categories["MCP"].push(tool.startsWith("mcp__") ? formatMcpName(tool) : tool); - } else if (isLikelyCustomAgent(tool)) { - categories["Custom Agents"].push(tool); - } else { - categories["Other"].push(tool); - } - } - for (const [category, tools] of Object.entries(categories)) { - if (tools.length > 0) { - markdown += `- **${category}:** ${tools.length} tools\n`; - markdown += ` - ${tools.join(", ")}\n`; - } - } - markdown += "\n"; - } - if (includeSlashCommands && initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { - const commandCount = initEntry.slash_commands.length; - markdown += `**Slash Commands:** ${commandCount} available\n`; - if (commandCount <= 10) { - markdown += `- ${initEntry.slash_commands.join(", ")}\n`; - } else { - markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; - } - markdown += "\n"; - } - if (mcpFailures.length > 0) { - return { markdown, mcpFailures }; - } - return { markdown }; - } - function formatToolUse(toolUse, toolResult, options = {}) { - const { includeDetailedParameters = false } = options; - const toolName = toolUse.name; - const input = toolUse.input || {}; - if (toolName === "TodoWrite") { - return ""; - } - function getStatusIcon() { - if (toolResult) { - return toolResult.is_error === true ? "❌" : "✅"; - } - return "❓"; - } - const statusIcon = getStatusIcon(); - let summary = ""; - let details = ""; - if (toolResult && toolResult.content) { - if (typeof toolResult.content === "string") { - details = toolResult.content; - } else if (Array.isArray(toolResult.content)) { - details = toolResult.content.map(c => (typeof c === "string" ? c : c.text || "")).join("\n"); - } - } - const inputText = JSON.stringify(input); - const outputText = details; - const totalTokens = estimateTokens(inputText) + estimateTokens(outputText); - let metadata = ""; - if (toolResult && toolResult.duration_ms) { - metadata += `${formatDuration(toolResult.duration_ms)} `; - } - if (totalTokens > 0) { - metadata += `~${totalTokens}t`; - } - metadata = metadata.trim(); - switch (toolName) { - case "Bash": - const command = input.command || ""; - const description = input.description || ""; - const formattedCommand = formatBashCommand(command); - if (description) { - summary = `${description}: ${formattedCommand}`; - } else { - summary = `${formattedCommand}`; - } - break; - case "Read": - const filePath = input.file_path || input.path || ""; - const relativePath = filePath.replace(/^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, ""); - summary = `Read ${relativePath}`; - break; - case "Write": - case "Edit": - case "MultiEdit": - const writeFilePath = input.file_path || input.path || ""; - const writeRelativePath = writeFilePath.replace(/^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, ""); - summary = `Write ${writeRelativePath}`; - break; - case "Grep": - case "Glob": - const query = input.query || input.pattern || ""; - summary = `Search for ${truncateString(query, 80)}`; - break; - case "LS": - const lsPath = input.path || ""; - const lsRelativePath = lsPath.replace(/^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, ""); - summary = `LS: ${lsRelativePath || lsPath}`; - break; - default: - if (toolName.startsWith("mcp__")) { - const mcpName = formatMcpName(toolName); - const params = formatMcpParameters(input); - summary = `${mcpName}(${params})`; - } else { - const keys = Object.keys(input); - if (keys.length > 0) { - const mainParam = keys.find(k => ["query", "command", "path", "file_path", "content"].includes(k)) || keys[0]; - const value = String(input[mainParam] || ""); - if (value) { - summary = `${toolName}: ${truncateString(value, 100)}`; - } else { - summary = toolName; - } - } else { - summary = toolName; - } - } - } - const sections = []; - if (includeDetailedParameters) { - const inputKeys = Object.keys(input); - if (inputKeys.length > 0) { - sections.push({ - label: "Parameters", - content: JSON.stringify(input, null, 2), - language: "json", - }); - } - } - if (details && details.trim()) { - sections.push({ - label: includeDetailedParameters ? "Response" : "Output", - content: details, - }); - } - return formatToolCallAsDetails({ - summary, - statusIcon, - sections, - metadata: metadata || undefined, - }); - } - function parseLogEntries(logContent) { - let logEntries; - try { - logEntries = JSON.parse(logContent); - if (!Array.isArray(logEntries) || logEntries.length === 0) { - throw new Error("Not a JSON array or empty array"); - } - return logEntries; - } catch (jsonArrayError) { - logEntries = []; - const lines = logContent.split("\n"); - for (const line of lines) { - const trimmedLine = line.trim(); - if (trimmedLine === "") { - continue; - } - if (trimmedLine.startsWith("[{")) { - try { - const arrayEntries = JSON.parse(trimmedLine); - if (Array.isArray(arrayEntries)) { - logEntries.push(...arrayEntries); - continue; - } - } catch (arrayParseError) { - continue; - } - } - if (!trimmedLine.startsWith("{")) { - continue; - } - try { - const jsonEntry = JSON.parse(trimmedLine); - logEntries.push(jsonEntry); - } catch (jsonLineError) { - continue; - } - } - } - if (!Array.isArray(logEntries) || logEntries.length === 0) { - return null; - } - return logEntries; - } - function formatToolCallAsDetails(options) { - const { summary, statusIcon, sections, metadata, maxContentLength = MAX_TOOL_OUTPUT_LENGTH } = options; - let fullSummary = summary; - if (statusIcon && !summary.startsWith(statusIcon)) { - fullSummary = `${statusIcon} ${summary}`; - } - if (metadata) { - fullSummary += ` ${metadata}`; - } - const hasContent = sections && sections.some(s => s.content && s.content.trim()); - if (!hasContent) { - return `${fullSummary}\n\n`; - } - let detailsContent = ""; - for (const section of sections) { - if (!section.content || !section.content.trim()) { - continue; - } - detailsContent += `**${section.label}:**\n\n`; - let content = section.content; - if (content.length > maxContentLength) { - content = content.substring(0, maxContentLength) + "... (truncated)"; - } - if (section.language) { - detailsContent += `\`\`\`\`\`\`${section.language}\n`; - } else { - detailsContent += "``````\n"; - } - detailsContent += content; - detailsContent += "\n``````\n\n"; - } - detailsContent = detailsContent.trimEnd(); - return `
\n${fullSummary}\n\n${detailsContent}\n
\n\n`; - } - function generatePlainTextSummary(logEntries, options = {}) { - const { model, parserName = "Agent" } = options; - const lines = []; - lines.push(`=== ${parserName} Execution Summary ===`); - if (model) { - lines.push(`Model: ${model}`); - } - lines.push(""); - const toolUsePairs = new Map(); - for (const entry of logEntries) { - if (entry.type === "user" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_result" && content.tool_use_id) { - toolUsePairs.set(content.tool_use_id, content); - } - } - } - } - lines.push("Conversation:"); - lines.push(""); - let conversationLineCount = 0; - const MAX_CONVERSATION_LINES = 50; - let conversationTruncated = false; - for (const entry of logEntries) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (content.type === "text" && content.text) { - const text = content.text.trim(); - if (text && text.length > 0) { - const maxTextLength = 500; - let displayText = text; - if (displayText.length > maxTextLength) { - displayText = displayText.substring(0, maxTextLength) + "..."; - } - const textLines = displayText.split("\n"); - for (const line of textLines) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - lines.push(`Agent: ${line}`); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } else if (content.type === "tool_use") { - const toolName = content.name; - const input = content.input || {}; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - const statusIcon = isError ? "✗" : "✓"; - let displayName; - let resultPreview = ""; - if (toolName === "Bash") { - const cmd = formatBashCommand(input.command || ""); - displayName = `$ ${cmd}`; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const resultLines = resultText.split("\n").filter(l => l.trim()); - if (resultLines.length > 0) { - const previewLine = resultLines[0].substring(0, 80); - if (resultLines.length > 1) { - resultPreview = ` └ ${resultLines.length} lines...`; - } else if (previewLine) { - resultPreview = ` └ ${previewLine}`; - } - } - } - } else if (toolName.startsWith("mcp__")) { - const formattedName = formatMcpName(toolName).replace("::", "-"); - displayName = formattedName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } else { - displayName = toolName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } - lines.push(`${statusIcon} ${displayName}`); - conversationLineCount++; - if (resultPreview) { - lines.push(resultPreview); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } - } - } - if (conversationTruncated) { - lines.push("... (conversation truncated)"); - lines.push(""); - } - const lastEntry = logEntries[logEntries.length - 1]; - lines.push("Statistics:"); - if (lastEntry?.num_turns) { - lines.push(` Turns: ${lastEntry.num_turns}`); - } - if (lastEntry?.duration_ms) { - const duration = formatDuration(lastEntry.duration_ms); - if (duration) { - lines.push(` Duration: ${duration}`); - } - } - let toolCounts = { total: 0, success: 0, error: 0 }; - for (const entry of logEntries) { - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_use") { - const toolName = content.name; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - toolCounts.total++; - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - if (isError) { - toolCounts.error++; - } else { - toolCounts.success++; - } - } - } - } - } - if (toolCounts.total > 0) { - lines.push(` Tools: ${toolCounts.success}/${toolCounts.total} succeeded`); - } - if (lastEntry?.usage) { - const usage = lastEntry.usage; - if (usage.input_tokens || usage.output_tokens) { - const inputTokens = usage.input_tokens || 0; - const outputTokens = usage.output_tokens || 0; - const cacheCreationTokens = usage.cache_creation_input_tokens || 0; - const cacheReadTokens = usage.cache_read_input_tokens || 0; - const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; - lines.push( - ` Tokens: ${totalTokens.toLocaleString()} total (${usage.input_tokens.toLocaleString()} in / ${usage.output_tokens.toLocaleString()} out)` - ); - } - } - if (lastEntry?.total_cost_usd) { - lines.push(` Cost: $${lastEntry.total_cost_usd.toFixed(4)}`); - } - return lines.join("\n"); - } - function generateCopilotCliStyleSummary(logEntries, options = {}) { - const { model, parserName = "Agent" } = options; - const lines = []; - const toolUsePairs = new Map(); - for (const entry of logEntries) { - if (entry.type === "user" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_result" && content.tool_use_id) { - toolUsePairs.set(content.tool_use_id, content); - } - } - } - } - lines.push("```"); - lines.push("Conversation:"); - lines.push(""); - let conversationLineCount = 0; - const MAX_CONVERSATION_LINES = 50; - let conversationTruncated = false; - for (const entry of logEntries) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (content.type === "text" && content.text) { - const text = content.text.trim(); - if (text && text.length > 0) { - const maxTextLength = 500; - let displayText = text; - if (displayText.length > maxTextLength) { - displayText = displayText.substring(0, maxTextLength) + "..."; - } - const textLines = displayText.split("\n"); - for (const line of textLines) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - lines.push(`Agent: ${line}`); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } else if (content.type === "tool_use") { - const toolName = content.name; - const input = content.input || {}; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - const statusIcon = isError ? "✗" : "✓"; - let displayName; - let resultPreview = ""; - if (toolName === "Bash") { - const cmd = formatBashCommand(input.command || ""); - displayName = `$ ${cmd}`; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const resultLines = resultText.split("\n").filter(l => l.trim()); - if (resultLines.length > 0) { - const previewLine = resultLines[0].substring(0, 80); - if (resultLines.length > 1) { - resultPreview = ` └ ${resultLines.length} lines...`; - } else if (previewLine) { - resultPreview = ` └ ${previewLine}`; - } - } - } - } else if (toolName.startsWith("mcp__")) { - const formattedName = formatMcpName(toolName).replace("::", "-"); - displayName = formattedName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } else { - displayName = toolName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } - lines.push(`${statusIcon} ${displayName}`); - conversationLineCount++; - if (resultPreview) { - lines.push(resultPreview); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } - } - } - if (conversationTruncated) { - lines.push("... (conversation truncated)"); - lines.push(""); - } - const lastEntry = logEntries[logEntries.length - 1]; - lines.push("Statistics:"); - if (lastEntry?.num_turns) { - lines.push(` Turns: ${lastEntry.num_turns}`); - } - if (lastEntry?.duration_ms) { - const duration = formatDuration(lastEntry.duration_ms); - if (duration) { - lines.push(` Duration: ${duration}`); - } - } - let toolCounts = { total: 0, success: 0, error: 0 }; - for (const entry of logEntries) { - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_use") { - const toolName = content.name; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - toolCounts.total++; - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - if (isError) { - toolCounts.error++; - } else { - toolCounts.success++; - } - } - } - } - } - if (toolCounts.total > 0) { - lines.push(` Tools: ${toolCounts.success}/${toolCounts.total} succeeded`); - } - if (lastEntry?.usage) { - const usage = lastEntry.usage; - if (usage.input_tokens || usage.output_tokens) { - const inputTokens = usage.input_tokens || 0; - const outputTokens = usage.output_tokens || 0; - const cacheCreationTokens = usage.cache_creation_input_tokens || 0; - const cacheReadTokens = usage.cache_read_input_tokens || 0; - const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; - lines.push( - ` Tokens: ${totalTokens.toLocaleString()} total (${usage.input_tokens.toLocaleString()} in / ${usage.output_tokens.toLocaleString()} out)` - ); - } - } - if (lastEntry?.total_cost_usd) { - lines.push(` Cost: $${lastEntry.total_cost_usd.toFixed(4)}`); - } - lines.push("```"); - return lines.join("\n"); - } - function runLogParser(options) { - const fs = require("fs"); - const path = require("path"); - const { parseLog, parserName, supportsDirectories = false } = options; - try { - const logPath = process.env.GH_AW_AGENT_OUTPUT; - if (!logPath) { - core.info("No agent log file specified"); - return; - } - if (!fs.existsSync(logPath)) { - core.info(`Log path not found: ${logPath}`); - return; - } - let content = ""; - const stat = fs.statSync(logPath); - if (stat.isDirectory()) { - if (!supportsDirectories) { - core.info(`Log path is a directory but ${parserName} parser does not support directories: ${logPath}`); - return; - } - const files = fs.readdirSync(logPath); - const logFiles = files.filter(file => file.endsWith(".log") || file.endsWith(".txt")); - if (logFiles.length === 0) { - core.info(`No log files found in directory: ${logPath}`); - return; - } - logFiles.sort(); - for (const file of logFiles) { - const filePath = path.join(logPath, file); - const fileContent = fs.readFileSync(filePath, "utf8"); - if (content.length > 0 && !content.endsWith("\n")) { - content += "\n"; - } - content += fileContent; - } - } else { - content = fs.readFileSync(logPath, "utf8"); - } - const result = parseLog(content); - let markdown = ""; - let mcpFailures = []; - let maxTurnsHit = false; - let logEntries = null; - if (typeof result === "string") { - markdown = result; - } else if (result && typeof result === "object") { - markdown = result.markdown || ""; - mcpFailures = result.mcpFailures || []; - maxTurnsHit = result.maxTurnsHit || false; - logEntries = result.logEntries || null; - } - if (markdown) { - if (logEntries && Array.isArray(logEntries) && logEntries.length > 0) { - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); - const model = initEntry?.model || null; - const plainTextSummary = generatePlainTextSummary(logEntries, { - model, - parserName, - }); - core.info(plainTextSummary); - const copilotCliStyleMarkdown = generateCopilotCliStyleSummary(logEntries, { - model, - parserName, - }); - core.summary.addRaw(copilotCliStyleMarkdown).write(); - } else { - core.info(`${parserName} log parsed successfully`); - core.summary.addRaw(markdown).write(); - } - } else { - core.error(`Failed to parse ${parserName} log`); - } - if (mcpFailures && mcpFailures.length > 0) { - const failedServers = mcpFailures.join(", "); - core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); - } - if (maxTurnsHit) { - core.setFailed(`Agent execution stopped: max-turns limit reached. The agent did not complete its task successfully.`); - } - } catch (error) { - core.setFailed(error instanceof Error ? error : String(error)); - } - } - function main() { - runLogParser({ - parseLog: parseCopilotLog, - parserName: "Copilot", - supportsDirectories: true, - }); - } - function extractPremiumRequestCount(logContent) { - const patterns = [ - /premium\s+requests?\s+consumed:?\s*(\d+)/i, - /(\d+)\s+premium\s+requests?\s+consumed/i, - /consumed\s+(\d+)\s+premium\s+requests?/i, - ]; - for (const pattern of patterns) { - const match = logContent.match(pattern); - if (match && match[1]) { - const count = parseInt(match[1], 10); - if (!isNaN(count) && count > 0) { - return count; - } - } - } - return 1; - } - function parseCopilotLog(logContent) { - try { - let logEntries; - try { - logEntries = JSON.parse(logContent); - if (!Array.isArray(logEntries)) { - throw new Error("Not a JSON array"); - } - } catch (jsonArrayError) { - const debugLogEntries = parseDebugLogFormat(logContent); - if (debugLogEntries && debugLogEntries.length > 0) { - logEntries = debugLogEntries; - } else { - logEntries = parseLogEntries(logContent); - } - } - if (!logEntries || logEntries.length === 0) { - return { markdown: "## Agent Log Summary\n\nLog format not recognized as Copilot JSON array or JSONL.\n", logEntries: [] }; - } - const conversationResult = generateConversationMarkdown(logEntries, { - formatToolCallback: (toolUse, toolResult) => formatToolUse(toolUse, toolResult, { includeDetailedParameters: true }), - formatInitCallback: initEntry => - formatInitializationSummary(initEntry, { - includeSlashCommands: false, - modelInfoCallback: entry => { - if (!entry.model_info) return ""; - const modelInfo = entry.model_info; - let markdown = ""; - if (modelInfo.name) { - markdown += `**Model Name:** ${modelInfo.name}`; - if (modelInfo.vendor) { - markdown += ` (${modelInfo.vendor})`; - } - markdown += "\n\n"; - } - if (modelInfo.billing) { - const billing = modelInfo.billing; - if (billing.is_premium === true) { - markdown += `**Premium Model:** Yes`; - if (billing.multiplier && billing.multiplier !== 1) { - markdown += ` (${billing.multiplier}x cost multiplier)`; - } - markdown += "\n"; - if (billing.restricted_to && Array.isArray(billing.restricted_to) && billing.restricted_to.length > 0) { - markdown += `**Required Plans:** ${billing.restricted_to.join(", ")}\n`; - } - markdown += "\n"; - } else if (billing.is_premium === false) { - markdown += `**Premium Model:** No\n\n`; - } - } - return markdown; - }, - }), - }); - let markdown = conversationResult.markdown; - const lastEntry = logEntries[logEntries.length - 1]; - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); - markdown += generateInformationSection(lastEntry, { - additionalInfoCallback: entry => { - const isPremiumModel = - initEntry && initEntry.model_info && initEntry.model_info.billing && initEntry.model_info.billing.is_premium === true; - if (isPremiumModel) { - const premiumRequestCount = extractPremiumRequestCount(logContent); - return `**Premium Requests Consumed:** ${premiumRequestCount}\n\n`; - } - return ""; - }, - }); - return { markdown, logEntries }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - return { - markdown: `## Agent Log Summary\n\nError parsing Copilot log (tried both JSON array and JSONL formats): ${errorMessage}\n`, - logEntries: [], - }; - } - } - function scanForToolErrors(logContent) { - const toolErrors = new Map(); - const lines = logContent.split("\n"); - const recentToolCalls = []; - const MAX_RECENT_TOOLS = 10; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes('"tool_calls":') && !line.includes('\\"tool_calls\\"')) { - for (let j = i + 1; j < Math.min(i + 30, lines.length); j++) { - const nextLine = lines[j]; - const idMatch = nextLine.match(/"id":\s*"([^"]+)"/); - const nameMatch = nextLine.match(/"name":\s*"([^"]+)"/) && !nextLine.includes('\\"name\\"'); - if (idMatch) { - const toolId = idMatch[1]; - for (let k = j; k < Math.min(j + 10, lines.length); k++) { - const nameLine = lines[k]; - const funcNameMatch = nameLine.match(/"name":\s*"([^"]+)"/); - if (funcNameMatch && !nameLine.includes('\\"name\\"')) { - const toolName = funcNameMatch[1]; - recentToolCalls.unshift({ id: toolId, name: toolName }); - if (recentToolCalls.length > MAX_RECENT_TOOLS) { - recentToolCalls.pop(); - } - break; - } - } - } - } - } - const errorMatch = line.match(/\[ERROR\].*(?:Tool execution failed|Permission denied|Resource not accessible|Error executing tool)/i); - if (errorMatch) { - const toolNameMatch = line.match(/Tool execution failed:\s*([^\s]+)/i); - const toolIdMatch = line.match(/tool_call_id:\s*([^\s]+)/i); - if (toolNameMatch) { - const toolName = toolNameMatch[1]; - toolErrors.set(toolName, true); - const matchingTool = recentToolCalls.find(t => t.name === toolName); - if (matchingTool) { - toolErrors.set(matchingTool.id, true); - } - } else if (toolIdMatch) { - toolErrors.set(toolIdMatch[1], true); - } else if (recentToolCalls.length > 0) { - const lastTool = recentToolCalls[0]; - toolErrors.set(lastTool.id, true); - toolErrors.set(lastTool.name, true); - } - } - } - return toolErrors; - } - function parseDebugLogFormat(logContent) { - const entries = []; - const lines = logContent.split("\n"); - const toolErrors = scanForToolErrors(logContent); - let model = "unknown"; - let sessionId = null; - let modelInfo = null; - let tools = []; - const modelMatch = logContent.match(/Starting Copilot CLI: ([\d.]+)/); - if (modelMatch) { - sessionId = `copilot-${modelMatch[1]}-${Date.now()}`; - } - const gotModelInfoIndex = logContent.indexOf("[DEBUG] Got model info: {"); - if (gotModelInfoIndex !== -1) { - const jsonStart = logContent.indexOf("{", gotModelInfoIndex); - if (jsonStart !== -1) { - let braceCount = 0; - let inString = false; - let escapeNext = false; - let jsonEnd = -1; - for (let i = jsonStart; i < logContent.length; i++) { - const char = logContent[i]; - if (escapeNext) { - escapeNext = false; - continue; - } - if (char === "\\") { - escapeNext = true; - continue; - } - if (char === '"' && !escapeNext) { - inString = !inString; - continue; - } - if (inString) continue; - if (char === "{") { - braceCount++; - } else if (char === "}") { - braceCount--; - if (braceCount === 0) { - jsonEnd = i + 1; - break; - } - } - } - if (jsonEnd !== -1) { - const modelInfoJson = logContent.substring(jsonStart, jsonEnd); - try { - modelInfo = JSON.parse(modelInfoJson); - } catch (e) { - } - } - } - } - const toolsIndex = logContent.indexOf("[DEBUG] Tools:"); - if (toolsIndex !== -1) { - const afterToolsLine = logContent.indexOf("\n", toolsIndex); - let toolsStart = logContent.indexOf("[DEBUG] [", afterToolsLine); - if (toolsStart !== -1) { - toolsStart = logContent.indexOf("[", toolsStart + 7); - } - if (toolsStart !== -1) { - let bracketCount = 0; - let inString = false; - let escapeNext = false; - let toolsEnd = -1; - for (let i = toolsStart; i < logContent.length; i++) { - const char = logContent[i]; - if (escapeNext) { - escapeNext = false; - continue; - } - if (char === "\\") { - escapeNext = true; - continue; - } - if (char === '"' && !escapeNext) { - inString = !inString; - continue; - } - if (inString) continue; - if (char === "[") { - bracketCount++; - } else if (char === "]") { - bracketCount--; - if (bracketCount === 0) { - toolsEnd = i + 1; - break; - } - } - } - if (toolsEnd !== -1) { - let toolsJson = logContent.substring(toolsStart, toolsEnd); - toolsJson = toolsJson.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /gm, ""); - try { - const toolsArray = JSON.parse(toolsJson); - if (Array.isArray(toolsArray)) { - tools = toolsArray - .map(tool => { - if (tool.type === "function" && tool.function && tool.function.name) { - let name = tool.function.name; - if (name.startsWith("github-")) { - name = "mcp__github__" + name.substring(7); - } else if (name.startsWith("safe_outputs-")) { - name = name; - } - return name; - } - return null; - }) - .filter(name => name !== null); - } - } catch (e) { - } - } - } - } - let inDataBlock = false; - let currentJsonLines = []; - let turnCount = 0; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes("[DEBUG] data:")) { - inDataBlock = true; - currentJsonLines = []; - continue; - } - if (inDataBlock) { - const hasTimestamp = line.match(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z /); - if (hasTimestamp) { - const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); - const isJsonContent = /^[{\[}\]"]/.test(cleanLine) || cleanLine.trim().startsWith('"'); - if (!isJsonContent) { - if (currentJsonLines.length > 0) { - try { - const jsonStr = currentJsonLines.join("\n"); - const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - const originalToolName = toolName; - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; - } - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - const hasError = toolErrors.has(toolId) || toolErrors.has(originalToolName); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: hasError ? "Permission denied or tool execution failed" : "", - is_error: hasError, - }); - } - } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { - entries.push({ - type: "user", - message: { content: toolResults }, - }); - } - } - } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, - }; - } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; - } - } - } catch (e) { - } - } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } - } else { - const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); - currentJsonLines.push(cleanLine); - } - } - } - if (inDataBlock && currentJsonLines.length > 0) { - try { - const jsonStr = currentJsonLines.join("\n"); - const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - const originalToolName = toolName; - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; - } - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - const hasError = toolErrors.has(toolId) || toolErrors.has(originalToolName); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: hasError ? "Permission denied or tool execution failed" : "", - is_error: hasError, - }); - } - } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { - entries.push({ - type: "user", - message: { content: toolResults }, - }); - } - } - } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, - }; - } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; - } - } - } catch (e) { - } - } - if (entries.length > 0) { - const initEntry = { - type: "system", - subtype: "init", - session_id: sessionId, - model: model, - tools: tools, - }; - if (modelInfo) { - initEntry.model_info = modelInfo; - } - entries.unshift(initEntry); - if (entries._lastResult) { - entries.push(entries._lastResult); - delete entries._lastResult; - } - } - return entries; - } - main(); - - name: Upload Agent Stdio - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: agent-stdio.log - path: /tmp/gh-aw/agent-stdio.log - if-no-files-found: warn - - name: Validate agent logs for errors - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - GH_AW_ERROR_PATTERNS: "[{\"id\":\"\",\"pattern\":\"::(error)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - error\"},{\"id\":\"\",\"pattern\":\"::(warning)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - warning\"},{\"id\":\"\",\"pattern\":\"::(notice)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - notice\"},{\"id\":\"\",\"pattern\":\"(ERROR|Error):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Generic ERROR messages\"},{\"id\":\"\",\"pattern\":\"(WARNING|Warning):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Generic WARNING messages\"},{\"id\":\"\",\"pattern\":\"(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\s+\\\\[(ERROR)\\\\]\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI timestamped ERROR messages\"},{\"id\":\"\",\"pattern\":\"(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\s+\\\\[(WARN|WARNING)\\\\]\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI timestamped WARNING messages\"},{\"id\":\"\",\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\]\\\\s+(CRITICAL|ERROR):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI bracketed critical/error messages with timestamp\"},{\"id\":\"\",\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\]\\\\s+(WARNING):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI bracketed warning messages with timestamp\"},{\"id\":\"\",\"pattern\":\"✗\\\\s+(.+)\",\"level_group\":0,\"message_group\":1,\"description\":\"Copilot CLI failed command indicator\"},{\"id\":\"\",\"pattern\":\"(?:command not found|not found):\\\\s*(.+)|(.+):\\\\s*(?:command not found|not found)\",\"level_group\":0,\"message_group\":0,\"description\":\"Shell command not found error\"},{\"id\":\"\",\"pattern\":\"Cannot find module\\\\s+['\\\"](.+)['\\\"]\",\"level_group\":0,\"message_group\":1,\"description\":\"Node.js module not found error\"},{\"id\":\"\",\"pattern\":\"Permission denied and could not request permission from user\",\"level_group\":0,\"message_group\":0,\"description\":\"Copilot CLI permission denied warning (user interaction required)\"},{\"id\":\"\",\"pattern\":\"\\\\berror\\\\b.*permission.*denied\",\"level_group\":0,\"message_group\":0,\"description\":\"Permission denied error (requires error context)\"},{\"id\":\"\",\"pattern\":\"\\\\berror\\\\b.*unauthorized\",\"level_group\":0,\"message_group\":0,\"description\":\"Unauthorized access error (requires error context)\"},{\"id\":\"\",\"pattern\":\"\\\\berror\\\\b.*forbidden\",\"level_group\":0,\"message_group\":0,\"description\":\"Forbidden access error (requires error context)\"}]" - with: - script: | - function main() { - const fs = require("fs"); - const path = require("path"); - core.info("Starting validate_errors.cjs script"); - const startTime = Date.now(); - try { - const logPath = process.env.GH_AW_AGENT_OUTPUT; - if (!logPath) { - throw new Error("GH_AW_AGENT_OUTPUT environment variable is required"); - } - core.info(`Log path: ${logPath}`); - if (!fs.existsSync(logPath)) { - core.info(`Log path not found: ${logPath}`); - core.info("No logs to validate - skipping error validation"); - return; - } - const patterns = getErrorPatternsFromEnv(); - if (patterns.length === 0) { - throw new Error("GH_AW_ERROR_PATTERNS environment variable is required and must contain at least one pattern"); - } - core.info(`Loaded ${patterns.length} error patterns`); - core.info(`Patterns: ${JSON.stringify(patterns.map(p => ({ description: p.description, pattern: p.pattern })))}`); - let content = ""; - const stat = fs.statSync(logPath); - if (stat.isDirectory()) { - const files = fs.readdirSync(logPath); - const logFiles = files.filter(file => file.endsWith(".log") || file.endsWith(".txt")); - if (logFiles.length === 0) { - core.info(`No log files found in directory: ${logPath}`); - return; - } - core.info(`Found ${logFiles.length} log files in directory`); - logFiles.sort(); - for (const file of logFiles) { - const filePath = path.join(logPath, file); - const fileContent = fs.readFileSync(filePath, "utf8"); - core.info(`Reading log file: ${file} (${fileContent.length} bytes)`); - content += fileContent; - if (content.length > 0 && !content.endsWith("\n")) { - content += "\n"; - } - } - } else { - content = fs.readFileSync(logPath, "utf8"); - core.info(`Read single log file (${content.length} bytes)`); - } - core.info(`Total log content size: ${content.length} bytes, ${content.split("\n").length} lines`); - const hasErrors = validateErrors(content, patterns); - const elapsedTime = Date.now() - startTime; - core.info(`Error validation completed in ${elapsedTime}ms`); - if (hasErrors) { - core.error("Errors detected in agent logs - continuing workflow step (not failing for now)"); - } else { - core.info("Error validation completed successfully"); - } - } catch (error) { - console.debug(error); - core.error(`Error validating log: ${error instanceof Error ? error.message : String(error)}`); - } - } - function getErrorPatternsFromEnv() { - const patternsEnv = process.env.GH_AW_ERROR_PATTERNS; - if (!patternsEnv) { - throw new Error("GH_AW_ERROR_PATTERNS environment variable is required"); - } - try { - const patterns = JSON.parse(patternsEnv); - if (!Array.isArray(patterns)) { - throw new Error("GH_AW_ERROR_PATTERNS must be a JSON array"); - } - return patterns; - } catch (e) { - throw new Error(`Failed to parse GH_AW_ERROR_PATTERNS as JSON: ${e instanceof Error ? e.message : String(e)}`); - } - } - function shouldSkipLine(line) { - const GITHUB_ACTIONS_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\s+/; - if (new RegExp(GITHUB_ACTIONS_TIMESTAMP.source + "GH_AW_ERROR_PATTERNS:").test(line)) { - return true; - } - if (/^\s+GH_AW_ERROR_PATTERNS:\s*\[/.test(line)) { - return true; - } - if (new RegExp(GITHUB_ACTIONS_TIMESTAMP.source + "env:").test(line)) { - return true; - } - return false; - } - function validateErrors(logContent, patterns) { - const lines = logContent.split("\n"); - let hasErrors = false; - const MAX_ITERATIONS_PER_LINE = 10000; - const ITERATION_WARNING_THRESHOLD = 1000; - const MAX_TOTAL_ERRORS = 100; - const MAX_LINE_LENGTH = 10000; - const TOP_SLOW_PATTERNS_COUNT = 5; - core.info(`Starting error validation with ${patterns.length} patterns and ${lines.length} lines`); - const validationStartTime = Date.now(); - let totalMatches = 0; - let patternStats = []; - for (let patternIndex = 0; patternIndex < patterns.length; patternIndex++) { - const pattern = patterns[patternIndex]; - const patternStartTime = Date.now(); - let patternMatches = 0; - let regex; - try { - regex = new RegExp(pattern.pattern, "g"); - core.info(`Pattern ${patternIndex + 1}/${patterns.length}: ${pattern.description || "Unknown"} - regex: ${pattern.pattern}`); - } catch (e) { - core.error(`invalid error regex pattern: ${pattern.pattern}`); - continue; - } - for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { - const line = lines[lineIndex]; - if (shouldSkipLine(line)) { - continue; - } - if (line.length > MAX_LINE_LENGTH) { - continue; - } - if (totalMatches >= MAX_TOTAL_ERRORS) { - core.warning(`Stopping error validation after finding ${totalMatches} matches (max: ${MAX_TOTAL_ERRORS})`); - break; - } - let match; - let iterationCount = 0; - let lastIndex = -1; - while ((match = regex.exec(line)) !== null) { - iterationCount++; - if (regex.lastIndex === lastIndex) { - core.error(`Infinite loop detected at line ${lineIndex + 1}! Pattern: ${pattern.pattern}, lastIndex stuck at ${lastIndex}`); - core.error(`Line content (truncated): ${truncateString(line, 200)}`); - break; - } - lastIndex = regex.lastIndex; - if (iterationCount === ITERATION_WARNING_THRESHOLD) { - core.warning( - `High iteration count (${iterationCount}) on line ${lineIndex + 1} with pattern: ${pattern.description || pattern.pattern}` - ); - core.warning(`Line content (truncated): ${truncateString(line, 200)}`); - } - if (iterationCount > MAX_ITERATIONS_PER_LINE) { - core.error(`Maximum iteration limit (${MAX_ITERATIONS_PER_LINE}) exceeded at line ${lineIndex + 1}! Pattern: ${pattern.pattern}`); - core.error(`Line content (truncated): ${truncateString(line, 200)}`); - core.error(`This likely indicates a problematic regex pattern. Skipping remaining matches on this line.`); - break; - } - 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); - } - patternMatches++; - totalMatches++; - } - if (iterationCount > 100) { - core.info(`Line ${lineIndex + 1} had ${iterationCount} matches for pattern: ${pattern.description || pattern.pattern}`); - } - } - const patternElapsed = Date.now() - patternStartTime; - patternStats.push({ - description: pattern.description || "Unknown", - pattern: pattern.pattern.substring(0, 50) + (pattern.pattern.length > 50 ? "..." : ""), - matches: patternMatches, - timeMs: patternElapsed, - }); - if (patternElapsed > 5000) { - core.warning(`Pattern "${pattern.description}" took ${patternElapsed}ms to process (${patternMatches} matches)`); - } - if (totalMatches >= MAX_TOTAL_ERRORS) { - core.warning(`Stopping pattern processing after finding ${totalMatches} matches (max: ${MAX_TOTAL_ERRORS})`); - break; - } - } - const validationElapsed = Date.now() - validationStartTime; - core.info(`Validation summary: ${totalMatches} total matches found in ${validationElapsed}ms`); - patternStats.sort((a, b) => b.timeMs - a.timeMs); - const topSlow = patternStats.slice(0, TOP_SLOW_PATTERNS_COUNT); - if (topSlow.length > 0 && topSlow[0].timeMs > 1000) { - core.info(`Top ${TOP_SLOW_PATTERNS_COUNT} slowest patterns:`); - topSlow.forEach((stat, idx) => { - core.info(` ${idx + 1}. "${stat.description}" - ${stat.timeMs}ms (${stat.matches} matches)`); - }); - } - core.info(`Error validation completed. Errors found: ${hasErrors}`); - return hasErrors; - } - function extractLevel(match, pattern) { - if (pattern.level_group && pattern.level_group > 0 && match[pattern.level_group]) { - return match[pattern.level_group]; - } - const fullMatch = match[0]; - if (fullMatch.toLowerCase().includes("error")) { - return "error"; - } else if (fullMatch.toLowerCase().includes("warn")) { - return "warning"; - } - return "unknown"; - } - function extractMessage(match, pattern, fullLine) { - if (pattern.message_group && pattern.message_group > 0 && match[pattern.message_group]) { - return match[pattern.message_group].trim(); - } - return match[0] || fullLine.trim(); - } - function truncateString(str, maxLength) { - if (!str) return ""; - if (str.length <= maxLength) return str; - return str.substring(0, maxLength) + "..."; - } - if (typeof module !== "undefined" && module.exports) { - module.exports = { - validateErrors, - extractLevel, - extractMessage, - getErrorPatternsFromEnv, - truncateString, - shouldSkipLine, - }; - } - if (typeof module === "undefined" || require.main === module) { - main(); - } - - pre_activation: - runs-on: ubuntu-slim - outputs: - activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} - steps: - - name: Check team membership for workflow - id: check_membership - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_REQUIRED_ROLES: - with: - script: | - function parseRequiredPermissions() { - const requiredPermissionsEnv = process.env.GH_AW_REQUIRED_ROLES; - return requiredPermissionsEnv ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") : []; - } - function parseAllowedBots() { - const allowedBotsEnv = process.env.GH_AW_ALLOWED_BOTS; - return allowedBotsEnv ? allowedBotsEnv.split(",").filter(b => b.trim() !== "") : []; - } - async function checkBotStatus(actor, owner, repo) { - try { - const isBot = actor.endsWith("[bot]"); - if (!isBot) { - return { isBot: false, isActive: false }; - } - core.info(`Checking if bot '${actor}' is active on ${owner}/${repo}`); - try { - const botPermission = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - core.info(`Bot '${actor}' is active with permission level: ${botPermission.data.permission}`); - return { isBot: true, isActive: true }; - } catch (botError) { - if (typeof botError === "object" && botError !== null && "status" in botError && botError.status === 404) { - core.warning(`Bot '${actor}' is not active/installed on ${owner}/${repo}`); - return { isBot: true, isActive: false }; - } - const errorMessage = botError instanceof Error ? botError.message : String(botError); - core.warning(`Failed to check bot status: ${errorMessage}`); - return { isBot: true, isActive: false, error: errorMessage }; - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Error checking bot status: ${errorMessage}`); - return { isBot: false, isActive: false, error: errorMessage }; - } - } - async function checkRepositoryPermission(actor, owner, repo, requiredPermissions) { - try { - core.info(`Checking if user '${actor}' has required permissions for ${owner}/${repo}`); - core.info(`Required permissions: ${requiredPermissions.join(", ")}`); - const repoPermission = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - const permission = repoPermission.data.permission; - core.info(`Repository permission level: ${permission}`); - for (const requiredPerm of requiredPermissions) { - if (permission === requiredPerm || (requiredPerm === "maintainer" && permission === "maintain")) { - core.info(`✅ User has ${permission} access to repository`); - return { authorized: true, permission: permission }; - } - } - core.warning(`User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}`); - return { authorized: false, permission: permission }; - } catch (repoError) { - const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); - core.warning(`Repository permission check failed: ${errorMessage}`); - return { authorized: false, error: errorMessage }; - } - } - async function main() { - const { eventName } = context; - const actor = context.actor; - const { owner, repo } = context.repo; - const requiredPermissions = parseRequiredPermissions(); - const allowedBots = parseAllowedBots(); - if (eventName === "workflow_dispatch") { - const hasWriteRole = requiredPermissions.includes("write"); - if (hasWriteRole) { - core.info(`✅ Event ${eventName} does not require validation (write role allowed)`); - core.setOutput("is_team_member", "true"); - core.setOutput("result", "safe_event"); - return; - } - core.info(`Event ${eventName} requires validation (write role not allowed)`); - } - const safeEvents = ["schedule"]; - if (safeEvents.includes(eventName)) { - core.info(`✅ Event ${eventName} does not require validation`); - core.setOutput("is_team_member", "true"); - core.setOutput("result", "safe_event"); - return; - } - if (!requiredPermissions || requiredPermissions.length === 0) { - core.warning("❌ Configuration error: Required permissions not specified. Contact repository administrator."); - core.setOutput("is_team_member", "false"); - core.setOutput("result", "config_error"); - core.setOutput("error_message", "Configuration error: Required permissions not specified"); - return; - } - const result = await checkRepositoryPermission(actor, owner, repo, requiredPermissions); - if (result.error) { - core.setOutput("is_team_member", "false"); - core.setOutput("result", "api_error"); - core.setOutput("error_message", `Repository permission check failed: ${result.error}`); - return; - } - if (result.authorized) { - core.setOutput("is_team_member", "true"); - core.setOutput("result", "authorized"); - core.setOutput("user_permission", result.permission); - } else { - if (allowedBots && allowedBots.length > 0) { - core.info(`Checking if actor '${actor}' is in allowed bots list: ${allowedBots.join(", ")}`); - if (allowedBots.includes(actor)) { - core.info(`Actor '${actor}' is in the allowed bots list`); - const botStatus = await checkBotStatus(actor, owner, repo); - if (botStatus.isBot && botStatus.isActive) { - core.info(`✅ Bot '${actor}' is active on the repository and authorized`); - core.setOutput("is_team_member", "true"); - core.setOutput("result", "authorized_bot"); - core.setOutput("user_permission", "bot"); - return; - } else if (botStatus.isBot && !botStatus.isActive) { - core.warning(`Bot '${actor}' is in the allowed list but not active/installed on ${owner}/${repo}`); - core.setOutput("is_team_member", "false"); - core.setOutput("result", "bot_not_active"); - core.setOutput("user_permission", result.permission); - core.setOutput("error_message", `Access denied: Bot '${actor}' is not active/installed on this repository`); - return; - } else { - core.info(`Actor '${actor}' is in allowed bots list but bot status check failed`); - } - } - } - core.setOutput("is_team_member", "false"); - core.setOutput("result", "insufficient_permissions"); - core.setOutput("user_permission", result.permission); - core.setOutput( - "error_message", - `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - } - } - await main(); - diff --git a/.github/workflows/org-modernization.campaign.g.lock.yml b/.github/workflows/org-modernization.campaign.g.lock.yml index 080cd6d208d..e9c2b7e31b8 100644 --- a/.github/workflows/org-modernization.campaign.g.lock.yml +++ b/.github/workflows/org-modernization.campaign.g.lock.yml @@ -2270,6 +2270,7 @@ jobs: env: GH_AW_REQUIRED_ROLES: admin,maintainer,write with: + github-token: ${{ secrets.GITHUB_TOKEN }} script: | function parseRequiredPermissions() { const requiredPermissionsEnv = process.env.GH_AW_REQUIRED_ROLES; diff --git a/.github/workflows/security-compliance-campaign.lock.yml b/.github/workflows/security-compliance-campaign.lock.yml deleted file mode 100644 index 78833dfd374..00000000000 --- a/.github/workflows/security-compliance-campaign.lock.yml +++ /dev/null @@ -1,2405 +0,0 @@ -# -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# 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 -# For more information: https://github.com/githubnext/gh-aw/blob/main/.github/aw/github-agentic-workflows.md -# -# Security remediation with compliance audit trail and executive reporting. -# -# Job Dependency Graph: -# ```mermaid -# graph LR -# activation["activation"] -# agent["agent"] -# pre_activation["pre_activation"] -# activation --> agent -# pre_activation --> activation -# ``` -# -# Original Prompt: -# ```markdown -# # Campaign Orchestrator -# -# This workflow orchestrates the 'Campaign: Security Compliance' campaign. -# -# - Tracker label: `campaign:security-compliance` -# - Associated workflows: security-compliance -# - Memory paths: memory/campaigns/security-compliance-*/** -# - Metrics glob: `memory/campaigns/security-compliance-*/metrics/*.json` -# -# Use these details to coordinate workers, update metrics, and track progress for this campaign. -# ``` -# -# Pinned GitHub Actions: -# - actions/github-script@v8 (ed597411d8f924073f98dfc5c65a23a2325f34cd) -# https://github.com/actions/github-script/commit/ed597411d8f924073f98dfc5c65a23a2325f34cd -# - actions/setup-node@v6 (395ad3262231945c25e8478fd5baf05154b1d79f) -# https://github.com/actions/setup-node/commit/395ad3262231945c25e8478fd5baf05154b1d79f -# - actions/upload-artifact@v5 (330a01c490aca151604b8cf639adc76d48f6c5d4) -# https://github.com/actions/upload-artifact/commit/330a01c490aca151604b8cf639adc76d48f6c5d4 - -name: "Campaign: Security Compliance" -on: - workflow_dispatch: - - -permissions: {} - - - - - -jobs: - activation: - needs: pre_activation - if: needs.pre_activation.outputs.activated == 'true' - runs-on: ubuntu-slim - permissions: - contents: read - outputs: - comment_id: "" - comment_repo: "" - steps: - - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_WORKFLOW_FILE: "security-compliance-campaign.lock.yml" - with: - script: | - async function main() { - const workflowFile = process.env.GH_AW_WORKFLOW_FILE; - if (!workflowFile) { - core.setFailed("Configuration error: GH_AW_WORKFLOW_FILE not available."); - return; - } - const workflowBasename = workflowFile.replace(".lock.yml", ""); - const workflowMdPath = `.github/workflows/${workflowBasename}.md`; - const lockFilePath = `.github/workflows/${workflowFile}`; - core.info(`Checking workflow timestamps using GitHub API:`); - core.info(` Source: ${workflowMdPath}`); - core.info(` Lock file: ${lockFilePath}`); - const { owner, repo } = context.repo; - const ref = context.sha; - async function getLastCommitForFile(path) { - try { - const response = await github.rest.repos.listCommits({ - owner, - repo, - path, - per_page: 1, - sha: ref, - }); - if (response.data && response.data.length > 0) { - const commit = response.data[0]; - return { - sha: commit.sha, - date: commit.commit.committer.date, - message: commit.commit.message, - }; - } - return null; - } catch (error) { - core.info(`Could not fetch commit for ${path}: ${error.message}`); - return null; - } - } - const workflowCommit = await getLastCommitForFile(workflowMdPath); - const lockCommit = await getLastCommitForFile(lockFilePath); - if (!workflowCommit) { - core.info(`Source file does not exist: ${workflowMdPath}`); - } - if (!lockCommit) { - core.info(`Lock file does not exist: ${lockFilePath}`); - } - if (!workflowCommit || !lockCommit) { - core.info("Skipping timestamp check - one or both files not found"); - return; - } - const workflowDate = new Date(workflowCommit.date); - const lockDate = new Date(lockCommit.date); - core.info(` Source last commit: ${workflowDate.toISOString()} (${workflowCommit.sha.substring(0, 7)})`); - core.info(` Lock last commit: ${lockDate.toISOString()} (${lockCommit.sha.substring(0, 7)})`); - if (workflowDate > lockDate) { - const warningMessage = `WARNING: Lock file '${lockFilePath}' is outdated! The workflow file '${workflowMdPath}' has been modified more recently. Run 'gh aw compile' to regenerate the lock file.`; - core.error(warningMessage); - const workflowTimestamp = workflowDate.toISOString(); - const lockTimestamp = lockDate.toISOString(); - let summary = core.summary - .addRaw("### ⚠️ Workflow Lock File Warning\n\n") - .addRaw("**WARNING**: Lock file is outdated and needs to be regenerated.\n\n") - .addRaw("**Files:**\n") - .addRaw(`- Source: \`${workflowMdPath}\`\n`) - .addRaw(` - Last commit: ${workflowTimestamp}\n`) - .addRaw( - ` - Commit SHA: [\`${workflowCommit.sha.substring(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${workflowCommit.sha})\n` - ) - .addRaw(`- Lock: \`${lockFilePath}\`\n`) - .addRaw(` - Last commit: ${lockTimestamp}\n`) - .addRaw(` - Commit SHA: [\`${lockCommit.sha.substring(0, 7)}\`](https://github.com/${owner}/${repo}/commit/${lockCommit.sha})\n\n`) - .addRaw("**Action Required:** Run `gh aw compile` to regenerate the lock file.\n\n"); - await summary.write(); - } else if (workflowCommit.sha === lockCommit.sha) { - core.info("✅ Lock file is up to date (same commit)"); - } else { - core.info("✅ Lock file is up to date"); - } - } - main().catch(error => { - core.setFailed(error instanceof Error ? error.message : String(error)); - }); - - agent: - needs: activation - outputs: - model: ${{ steps.generate_aw_info.outputs.model }} - steps: - - name: Create gh-aw temp directory - run: | - mkdir -p /tmp/gh-aw/agent - mkdir -p /tmp/gh-aw/sandbox/agent/logs - echo "Created /tmp/gh-aw/agent directory for agentic workflow temporary files" - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Validate COPILOT_GITHUB_TOKEN secret - run: | - if [ -z "$COPILOT_GITHUB_TOKEN" ]; then - { - echo "❌ Error: None of the following secrets are set: COPILOT_GITHUB_TOKEN" - echo "The GitHub Copilot CLI engine requires either COPILOT_GITHUB_TOKEN secret to be configured." - echo "Please configure one of these secrets in your repository settings." - echo "Documentation: https://githubnext.github.io/gh-aw/reference/engines/#github-copilot-default" - } >> "$GITHUB_STEP_SUMMARY" - echo "Error: None of the following secrets are set: COPILOT_GITHUB_TOKEN" - echo "The GitHub Copilot CLI engine requires either COPILOT_GITHUB_TOKEN secret to be configured." - echo "Please configure one of these secrets in your repository settings." - echo "Documentation: https://githubnext.github.io/gh-aw/reference/engines/#github-copilot-default" - exit 1 - fi - - # Log success to stdout (not step summary) - if [ -n "$COPILOT_GITHUB_TOKEN" ]; then - echo "COPILOT_GITHUB_TOKEN secret is configured" - fi - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Setup Node.js - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 - with: - node-version: '24' - package-manager-cache: false - - name: Install GitHub Copilot CLI - run: npm install -g @github/copilot@0.0.369 - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "copilot", - engine_name: "GitHub Copilot CLI", - model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", - version: "", - agent_version: "0.0.369", - workflow_name: "Campaign: Security Compliance", - experimental: false, - supports_tools_allowlist: true, - supports_http_transport: true, - 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, - network_mode: "defaults", - allowed_domains: [], - firewall_enabled: false, - firewall_version: "", - steps: { - firewall: "" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/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)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); - - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require('fs'); - const awInfoPath = '/tmp/gh-aw/aw_info.json'; - - // Load aw_info.json - const awInfo = JSON.parse(fs.readFileSync(awInfoPath, 'utf8')); - - let networkDetails = ''; - if (awInfo.allowed_domains && awInfo.allowed_domains.length > 0) { - networkDetails = awInfo.allowed_domains.slice(0, 10).map(d => ` - ${d}`).join('\n'); - if (awInfo.allowed_domains.length > 10) { - networkDetails += `\n - ... and ${awInfo.allowed_domains.length - 10} more`; - } - } - - const summary = '
\n' + - 'Run details\n\n' + - '#### Engine Configuration\n' + - '| Property | Value |\n' + - '|----------|-------|\n' + - `| Engine ID | ${awInfo.engine_id} |\n` + - `| Engine Name | ${awInfo.engine_name} |\n` + - `| Model | ${awInfo.model || '(default)'} |\n` + - '\n' + - '#### Network Configuration\n' + - '| Property | Value |\n' + - '|----------|-------|\n' + - `| Mode | ${awInfo.network_mode || 'defaults'} |\n` + - `| Firewall | ${awInfo.firewall_enabled ? '✅ Enabled' : '❌ Disabled'} |\n` + - `| Firewall Version | ${awInfo.firewall_version || '(latest)'} |\n` + - '\n' + - (networkDetails ? `##### Allowed Domains\n${networkDetails}\n` : '') + - '
'; - - await core.summary.addRaw(summary).write(); - console.log('Generated workflow overview in step summary'); - - name: Create prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: | - PROMPT_DIR="$(dirname "$GH_AW_PROMPT")" - mkdir -p "$PROMPT_DIR" - cat << 'PROMPT_EOF' > "$GH_AW_PROMPT" - # Campaign Orchestrator - - This workflow orchestrates the 'Campaign: Security Compliance' campaign. - - - Tracker label: `campaign:security-compliance` - - Associated workflows: security-compliance - - Memory paths: memory/campaigns/security-compliance-*/** - - Metrics glob: `memory/campaigns/security-compliance-*/metrics/*.json` - - Use these details to coordinate workers, update metrics, and track progress for this campaign. - - PROMPT_EOF - - name: Append temporary folder instructions to prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: | - cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" - - /tmp/gh-aw/agent/ - When you need to create temporary files or directories during your work, always use the /tmp/gh-aw/agent/ directory that has been pre-created for you. Do NOT use the root /tmp/ directory directly. - - - PROMPT_EOF - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: | - # Print prompt to workflow logs (equivalent to core.info) - echo "Generated Prompt:" - cat "$GH_AW_PROMPT" - # Print prompt to step summary - { - echo "
" - echo "Generated Prompt" - echo "" - echo '``````markdown' - cat "$GH_AW_PROMPT" - echo '``````' - echo "" - echo "
" - } >> "$GITHUB_STEP_SUMMARY" - - name: Upload prompt - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: prompt.txt - path: /tmp/gh-aw/aw-prompts/prompt.txt - if-no-files-found: warn - - name: Upload agentic run info - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: aw_info.json - path: /tmp/gh-aw/aw_info.json - if-no-files-found: warn - - name: Execute GitHub Copilot CLI - id: agentic_execution - timeout-minutes: 20 - run: | - set -o pipefail - COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" - mkdir -p /tmp/ - mkdir -p /tmp/gh-aw/ - mkdir -p /tmp/gh-aw/agent/ - mkdir -p /tmp/gh-aw/sandbox/agent/logs/ - copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/agent-stdio.log - env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - GITHUB_WORKSPACE: ${{ github.workspace }} - XDG_CONFIG_HOME: /home/runner - - name: Redact secrets in logs - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require("fs"); - const path = require("path"); - function findFiles(dir, extensions) { - const results = []; - try { - if (!fs.existsSync(dir)) { - return results; - } - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - results.push(...findFiles(fullPath, extensions)); - } else if (entry.isFile()) { - const ext = path.extname(entry.name).toLowerCase(); - if (extensions.includes(ext)) { - results.push(fullPath); - } - } - } - } catch (error) { - core.warning(`Failed to scan directory ${dir}: ${error instanceof Error ? error.message : String(error)}`); - } - return results; - } - function redactSecrets(content, secretValues) { - let redactionCount = 0; - let redacted = content; - const sortedSecrets = secretValues.slice().sort((a, b) => b.length - a.length); - for (const secretValue of sortedSecrets) { - if (!secretValue || secretValue.length < 8) { - continue; - } - const prefix = secretValue.substring(0, 3); - const asterisks = "*".repeat(Math.max(0, secretValue.length - 3)); - const replacement = prefix + asterisks; - const parts = redacted.split(secretValue); - const occurrences = parts.length - 1; - if (occurrences > 0) { - redacted = parts.join(replacement); - redactionCount += occurrences; - core.info(`Redacted ${occurrences} occurrence(s) of a secret`); - } - } - return { content: redacted, redactionCount }; - } - function processFile(filePath, secretValues) { - try { - const content = fs.readFileSync(filePath, "utf8"); - const { content: redactedContent, redactionCount } = redactSecrets(content, secretValues); - if (redactionCount > 0) { - fs.writeFileSync(filePath, redactedContent, "utf8"); - core.info(`Processed ${filePath}: ${redactionCount} redaction(s)`); - } - return redactionCount; - } catch (error) { - core.warning(`Failed to process file ${filePath}: ${error instanceof Error ? error.message : String(error)}`); - return 0; - } - } - async function main() { - const secretNames = process.env.GH_AW_SECRET_NAMES; - if (!secretNames) { - core.info("GH_AW_SECRET_NAMES not set, no redaction performed"); - return; - } - core.info("Starting secret redaction in /tmp/gh-aw directory"); - try { - const secretNameList = secretNames.split(",").filter(name => name.trim()); - const secretValues = []; - for (const secretName of secretNameList) { - const envVarName = `SECRET_${secretName}`; - const secretValue = process.env[envVarName]; - if (!secretValue || secretValue.trim() === "") { - continue; - } - secretValues.push(secretValue.trim()); - } - if (secretValues.length === 0) { - core.info("No secret values found to redact"); - return; - } - core.info(`Found ${secretValues.length} secret(s) to redact`); - const targetExtensions = [".txt", ".json", ".log", ".md", ".mdx", ".yml", ".jsonl"]; - const files = findFiles("/tmp/gh-aw", targetExtensions); - core.info(`Found ${files.length} file(s) to scan for secrets`); - let totalRedactions = 0; - let filesWithRedactions = 0; - for (const file of files) { - const redactionCount = processFile(file, secretValues); - if (redactionCount > 0) { - filesWithRedactions++; - totalRedactions += redactionCount; - } - } - if (totalRedactions > 0) { - core.info(`Secret redaction complete: ${totalRedactions} redaction(s) in ${filesWithRedactions} file(s)`); - } else { - core.info("Secret redaction complete: no secrets found"); - } - } catch (error) { - core.setFailed(`Secret redaction failed: ${error instanceof Error ? error.message : String(error)}`); - } - } - await main(); - env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Upload engine output files - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: agent_outputs - path: | - /tmp/gh-aw/sandbox/agent/logs/ - /tmp/gh-aw/redacted-urls.log - if-no-files-found: ignore - - name: Upload MCP logs - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: mcp-logs - path: /tmp/gh-aw/mcp-logs/ - if-no-files-found: ignore - - name: Parse agent logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - with: - script: | - const MAX_TOOL_OUTPUT_LENGTH = 256; - const MAX_STEP_SUMMARY_SIZE = 1000 * 1024; - const MAX_BASH_COMMAND_DISPLAY_LENGTH = 40; - const SIZE_LIMIT_WARNING = "\n\n⚠️ *Step summary size limit reached. Additional content truncated.*\n\n"; - class StepSummaryTracker { - constructor(maxSize = MAX_STEP_SUMMARY_SIZE) { - this.currentSize = 0; - this.maxSize = maxSize; - this.limitReached = false; - } - add(content) { - if (this.limitReached) { - return false; - } - const contentSize = Buffer.byteLength(content, "utf8"); - if (this.currentSize + contentSize > this.maxSize) { - this.limitReached = true; - return false; - } - this.currentSize += contentSize; - return true; - } - isLimitReached() { - return this.limitReached; - } - getSize() { - return this.currentSize; - } - reset() { - this.currentSize = 0; - this.limitReached = false; - } - } - function formatDuration(ms) { - if (!ms || ms <= 0) return ""; - const seconds = Math.round(ms / 1000); - if (seconds < 60) { - return `${seconds}s`; - } - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - if (remainingSeconds === 0) { - return `${minutes}m`; - } - return `${minutes}m ${remainingSeconds}s`; - } - function formatBashCommand(command) { - if (!command) return ""; - let formatted = command - .replace(/\n/g, " ") - .replace(/\r/g, " ") - .replace(/\t/g, " ") - .replace(/\s+/g, " ") - .trim(); - formatted = formatted.replace(/`/g, "\\`"); - const maxLength = 300; - if (formatted.length > maxLength) { - formatted = formatted.substring(0, maxLength) + "..."; - } - return formatted; - } - function truncateString(str, maxLength) { - if (!str) return ""; - if (str.length <= maxLength) return str; - return str.substring(0, maxLength) + "..."; - } - function estimateTokens(text) { - if (!text) return 0; - return Math.ceil(text.length / 4); - } - function formatMcpName(toolName) { - if (toolName.startsWith("mcp__")) { - const parts = toolName.split("__"); - if (parts.length >= 3) { - const provider = parts[1]; - const method = parts.slice(2).join("_"); - return `${provider}::${method}`; - } - } - return toolName; - } - function isLikelyCustomAgent(toolName) { - if (!toolName || typeof toolName !== "string") { - return false; - } - if (!toolName.includes("-")) { - return false; - } - if (toolName.includes("__")) { - return false; - } - if (toolName.toLowerCase().startsWith("safe")) { - return false; - } - if (!/^[a-z0-9]+(-[a-z0-9]+)+$/.test(toolName)) { - return false; - } - return true; - } - function generateConversationMarkdown(logEntries, options) { - const { formatToolCallback, formatInitCallback, summaryTracker } = options; - const toolUsePairs = new Map(); - for (const entry of logEntries) { - if (entry.type === "user" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_result" && content.tool_use_id) { - toolUsePairs.set(content.tool_use_id, content); - } - } - } - } - let markdown = ""; - let sizeLimitReached = false; - function addContent(content) { - if (summaryTracker && !summaryTracker.add(content)) { - sizeLimitReached = true; - return false; - } - markdown += content; - return true; - } - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); - if (initEntry && formatInitCallback) { - if (!addContent("## 🚀 Initialization\n\n")) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - const initResult = formatInitCallback(initEntry); - if (typeof initResult === "string") { - if (!addContent(initResult)) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - } else if (initResult && initResult.markdown) { - if (!addContent(initResult.markdown)) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - } - if (!addContent("\n")) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - } - if (!addContent("\n## 🤖 Reasoning\n\n")) { - return { markdown, commandSummary: [], sizeLimitReached }; - } - for (const entry of logEntries) { - if (sizeLimitReached) break; - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (sizeLimitReached) break; - if (content.type === "text" && content.text) { - const text = content.text.trim(); - if (text && text.length > 0) { - if (!addContent(text + "\n\n")) { - break; - } - } - } else if (content.type === "tool_use") { - const toolResult = toolUsePairs.get(content.id); - const toolMarkdown = formatToolCallback(content, toolResult); - if (toolMarkdown) { - if (!addContent(toolMarkdown)) { - break; - } - } - } - } - } - } - if (sizeLimitReached) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary: [], sizeLimitReached }; - } - if (!addContent("## 🤖 Commands and Tools\n\n")) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary: [], sizeLimitReached: true }; - } - const commandSummary = []; - for (const entry of logEntries) { - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_use") { - const toolName = content.name; - const input = content.input || {}; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - const toolResult = toolUsePairs.get(content.id); - let statusIcon = "❓"; - if (toolResult) { - statusIcon = toolResult.is_error === true ? "❌" : "✅"; - } - if (toolName === "Bash") { - const formattedCommand = formatBashCommand(input.command || ""); - commandSummary.push(`* ${statusIcon} \`${formattedCommand}\``); - } else if (toolName.startsWith("mcp__")) { - const mcpName = formatMcpName(toolName); - commandSummary.push(`* ${statusIcon} \`${mcpName}(...)\``); - } else { - commandSummary.push(`* ${statusIcon} ${toolName}`); - } - } - } - } - } - if (commandSummary.length > 0) { - for (const cmd of commandSummary) { - if (!addContent(`${cmd}\n`)) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary, sizeLimitReached: true }; - } - } - } else { - if (!addContent("No commands or tools used.\n")) { - markdown += SIZE_LIMIT_WARNING; - return { markdown, commandSummary, sizeLimitReached: true }; - } - } - return { markdown, commandSummary, sizeLimitReached }; - } - function generateInformationSection(lastEntry, options = {}) { - const { additionalInfoCallback } = options; - let markdown = "\n## 📊 Information\n\n"; - if (!lastEntry) { - return markdown; - } - if (lastEntry.num_turns) { - markdown += `**Turns:** ${lastEntry.num_turns}\n\n`; - } - if (lastEntry.duration_ms) { - const durationSec = Math.round(lastEntry.duration_ms / 1000); - const minutes = Math.floor(durationSec / 60); - const seconds = durationSec % 60; - markdown += `**Duration:** ${minutes}m ${seconds}s\n\n`; - } - if (lastEntry.total_cost_usd) { - markdown += `**Total Cost:** $${lastEntry.total_cost_usd.toFixed(4)}\n\n`; - } - if (additionalInfoCallback) { - const additionalInfo = additionalInfoCallback(lastEntry); - if (additionalInfo) { - markdown += additionalInfo; - } - } - if (lastEntry.usage) { - const usage = lastEntry.usage; - if (usage.input_tokens || usage.output_tokens) { - const inputTokens = usage.input_tokens || 0; - const outputTokens = usage.output_tokens || 0; - const cacheCreationTokens = usage.cache_creation_input_tokens || 0; - const cacheReadTokens = usage.cache_read_input_tokens || 0; - const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; - markdown += `**Token Usage:**\n`; - if (totalTokens > 0) markdown += `- Total: ${totalTokens.toLocaleString()}\n`; - if (usage.input_tokens) markdown += `- Input: ${usage.input_tokens.toLocaleString()}\n`; - if (usage.cache_creation_input_tokens) markdown += `- Cache Creation: ${usage.cache_creation_input_tokens.toLocaleString()}\n`; - if (usage.cache_read_input_tokens) markdown += `- Cache Read: ${usage.cache_read_input_tokens.toLocaleString()}\n`; - if (usage.output_tokens) markdown += `- Output: ${usage.output_tokens.toLocaleString()}\n`; - markdown += "\n"; - } - } - if (lastEntry.permission_denials && lastEntry.permission_denials.length > 0) { - markdown += `**Permission Denials:** ${lastEntry.permission_denials.length}\n\n`; - } - return markdown; - } - function formatMcpParameters(input) { - const keys = Object.keys(input); - if (keys.length === 0) return ""; - const paramStrs = []; - for (const key of keys.slice(0, 4)) { - const value = String(input[key] || ""); - paramStrs.push(`${key}: ${truncateString(value, 40)}`); - } - if (keys.length > 4) { - paramStrs.push("..."); - } - return paramStrs.join(", "); - } - function formatInitializationSummary(initEntry, options = {}) { - const { mcpFailureCallback, modelInfoCallback, includeSlashCommands = false } = options; - let markdown = ""; - const mcpFailures = []; - if (initEntry.model) { - markdown += `**Model:** ${initEntry.model}\n\n`; - } - if (modelInfoCallback) { - const modelInfo = modelInfoCallback(initEntry); - if (modelInfo) { - markdown += modelInfo; - } - } - if (initEntry.session_id) { - markdown += `**Session ID:** ${initEntry.session_id}\n\n`; - } - if (initEntry.cwd) { - const cleanCwd = initEntry.cwd.replace(/^\/home\/runner\/work\/[^\/]+\/[^\/]+/, "."); - markdown += `**Working Directory:** ${cleanCwd}\n\n`; - } - if (initEntry.mcp_servers && Array.isArray(initEntry.mcp_servers)) { - markdown += "**MCP Servers:**\n"; - for (const server of initEntry.mcp_servers) { - const statusIcon = server.status === "connected" ? "✅" : server.status === "failed" ? "❌" : "❓"; - markdown += `- ${statusIcon} ${server.name} (${server.status})\n`; - if (server.status === "failed") { - mcpFailures.push(server.name); - if (mcpFailureCallback) { - const failureDetails = mcpFailureCallback(server); - if (failureDetails) { - markdown += failureDetails; - } - } - } - } - markdown += "\n"; - } - if (initEntry.tools && Array.isArray(initEntry.tools)) { - markdown += "**Available Tools:**\n"; - const categories = { - Core: [], - "File Operations": [], - Builtin: [], - "Safe Outputs": [], - "Safe Inputs": [], - "Git/GitHub": [], - Playwright: [], - Serena: [], - MCP: [], - "Custom Agents": [], - Other: [], - }; - const builtinTools = [ - "bash", - "write_bash", - "read_bash", - "stop_bash", - "list_bash", - "grep", - "glob", - "view", - "create", - "edit", - "store_memory", - "code_review", - "codeql_checker", - "report_progress", - "report_intent", - "gh-advisory-database", - ]; - const internalTools = ["fetch_copilot_cli_documentation"]; - for (const tool of initEntry.tools) { - const toolLower = tool.toLowerCase(); - if (["Task", "Bash", "BashOutput", "KillBash", "ExitPlanMode"].includes(tool)) { - categories["Core"].push(tool); - } else if (["Read", "Edit", "MultiEdit", "Write", "LS", "Grep", "Glob", "NotebookEdit"].includes(tool)) { - categories["File Operations"].push(tool); - } else if (builtinTools.includes(toolLower) || internalTools.includes(toolLower)) { - categories["Builtin"].push(tool); - } else if (tool.startsWith("safeoutputs-") || tool.startsWith("safe_outputs-")) { - const toolName = tool.replace(/^safeoutputs-|^safe_outputs-/, ""); - categories["Safe Outputs"].push(toolName); - } else if (tool.startsWith("safeinputs-") || tool.startsWith("safe_inputs-")) { - const toolName = tool.replace(/^safeinputs-|^safe_inputs-/, ""); - categories["Safe Inputs"].push(toolName); - } else if (tool.startsWith("mcp__github__")) { - categories["Git/GitHub"].push(formatMcpName(tool)); - } else if (tool.startsWith("mcp__playwright__")) { - categories["Playwright"].push(formatMcpName(tool)); - } else if (tool.startsWith("mcp__serena__")) { - categories["Serena"].push(formatMcpName(tool)); - } else if (tool.startsWith("mcp__") || ["ListMcpResourcesTool", "ReadMcpResourceTool"].includes(tool)) { - categories["MCP"].push(tool.startsWith("mcp__") ? formatMcpName(tool) : tool); - } else if (isLikelyCustomAgent(tool)) { - categories["Custom Agents"].push(tool); - } else { - categories["Other"].push(tool); - } - } - for (const [category, tools] of Object.entries(categories)) { - if (tools.length > 0) { - markdown += `- **${category}:** ${tools.length} tools\n`; - markdown += ` - ${tools.join(", ")}\n`; - } - } - markdown += "\n"; - } - if (includeSlashCommands && initEntry.slash_commands && Array.isArray(initEntry.slash_commands)) { - const commandCount = initEntry.slash_commands.length; - markdown += `**Slash Commands:** ${commandCount} available\n`; - if (commandCount <= 10) { - markdown += `- ${initEntry.slash_commands.join(", ")}\n`; - } else { - markdown += `- ${initEntry.slash_commands.slice(0, 5).join(", ")}, and ${commandCount - 5} more\n`; - } - markdown += "\n"; - } - if (mcpFailures.length > 0) { - return { markdown, mcpFailures }; - } - return { markdown }; - } - function formatToolUse(toolUse, toolResult, options = {}) { - const { includeDetailedParameters = false } = options; - const toolName = toolUse.name; - const input = toolUse.input || {}; - if (toolName === "TodoWrite") { - return ""; - } - function getStatusIcon() { - if (toolResult) { - return toolResult.is_error === true ? "❌" : "✅"; - } - return "❓"; - } - const statusIcon = getStatusIcon(); - let summary = ""; - let details = ""; - if (toolResult && toolResult.content) { - if (typeof toolResult.content === "string") { - details = toolResult.content; - } else if (Array.isArray(toolResult.content)) { - details = toolResult.content.map(c => (typeof c === "string" ? c : c.text || "")).join("\n"); - } - } - const inputText = JSON.stringify(input); - const outputText = details; - const totalTokens = estimateTokens(inputText) + estimateTokens(outputText); - let metadata = ""; - if (toolResult && toolResult.duration_ms) { - metadata += `${formatDuration(toolResult.duration_ms)} `; - } - if (totalTokens > 0) { - metadata += `~${totalTokens}t`; - } - metadata = metadata.trim(); - switch (toolName) { - case "Bash": - const command = input.command || ""; - const description = input.description || ""; - const formattedCommand = formatBashCommand(command); - if (description) { - summary = `${description}: ${formattedCommand}`; - } else { - summary = `${formattedCommand}`; - } - break; - case "Read": - const filePath = input.file_path || input.path || ""; - const relativePath = filePath.replace(/^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, ""); - summary = `Read ${relativePath}`; - break; - case "Write": - case "Edit": - case "MultiEdit": - const writeFilePath = input.file_path || input.path || ""; - const writeRelativePath = writeFilePath.replace(/^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, ""); - summary = `Write ${writeRelativePath}`; - break; - case "Grep": - case "Glob": - const query = input.query || input.pattern || ""; - summary = `Search for ${truncateString(query, 80)}`; - break; - case "LS": - const lsPath = input.path || ""; - const lsRelativePath = lsPath.replace(/^\/[^\/]*\/[^\/]*\/[^\/]*\/[^\/]*\//, ""); - summary = `LS: ${lsRelativePath || lsPath}`; - break; - default: - if (toolName.startsWith("mcp__")) { - const mcpName = formatMcpName(toolName); - const params = formatMcpParameters(input); - summary = `${mcpName}(${params})`; - } else { - const keys = Object.keys(input); - if (keys.length > 0) { - const mainParam = keys.find(k => ["query", "command", "path", "file_path", "content"].includes(k)) || keys[0]; - const value = String(input[mainParam] || ""); - if (value) { - summary = `${toolName}: ${truncateString(value, 100)}`; - } else { - summary = toolName; - } - } else { - summary = toolName; - } - } - } - const sections = []; - if (includeDetailedParameters) { - const inputKeys = Object.keys(input); - if (inputKeys.length > 0) { - sections.push({ - label: "Parameters", - content: JSON.stringify(input, null, 2), - language: "json", - }); - } - } - if (details && details.trim()) { - sections.push({ - label: includeDetailedParameters ? "Response" : "Output", - content: details, - }); - } - return formatToolCallAsDetails({ - summary, - statusIcon, - sections, - metadata: metadata || undefined, - }); - } - function parseLogEntries(logContent) { - let logEntries; - try { - logEntries = JSON.parse(logContent); - if (!Array.isArray(logEntries) || logEntries.length === 0) { - throw new Error("Not a JSON array or empty array"); - } - return logEntries; - } catch (jsonArrayError) { - logEntries = []; - const lines = logContent.split("\n"); - for (const line of lines) { - const trimmedLine = line.trim(); - if (trimmedLine === "") { - continue; - } - if (trimmedLine.startsWith("[{")) { - try { - const arrayEntries = JSON.parse(trimmedLine); - if (Array.isArray(arrayEntries)) { - logEntries.push(...arrayEntries); - continue; - } - } catch (arrayParseError) { - continue; - } - } - if (!trimmedLine.startsWith("{")) { - continue; - } - try { - const jsonEntry = JSON.parse(trimmedLine); - logEntries.push(jsonEntry); - } catch (jsonLineError) { - continue; - } - } - } - if (!Array.isArray(logEntries) || logEntries.length === 0) { - return null; - } - return logEntries; - } - function formatToolCallAsDetails(options) { - const { summary, statusIcon, sections, metadata, maxContentLength = MAX_TOOL_OUTPUT_LENGTH } = options; - let fullSummary = summary; - if (statusIcon && !summary.startsWith(statusIcon)) { - fullSummary = `${statusIcon} ${summary}`; - } - if (metadata) { - fullSummary += ` ${metadata}`; - } - const hasContent = sections && sections.some(s => s.content && s.content.trim()); - if (!hasContent) { - return `${fullSummary}\n\n`; - } - let detailsContent = ""; - for (const section of sections) { - if (!section.content || !section.content.trim()) { - continue; - } - detailsContent += `**${section.label}:**\n\n`; - let content = section.content; - if (content.length > maxContentLength) { - content = content.substring(0, maxContentLength) + "... (truncated)"; - } - if (section.language) { - detailsContent += `\`\`\`\`\`\`${section.language}\n`; - } else { - detailsContent += "``````\n"; - } - detailsContent += content; - detailsContent += "\n``````\n\n"; - } - detailsContent = detailsContent.trimEnd(); - return `
\n${fullSummary}\n\n${detailsContent}\n
\n\n`; - } - function generatePlainTextSummary(logEntries, options = {}) { - const { model, parserName = "Agent" } = options; - const lines = []; - lines.push(`=== ${parserName} Execution Summary ===`); - if (model) { - lines.push(`Model: ${model}`); - } - lines.push(""); - const toolUsePairs = new Map(); - for (const entry of logEntries) { - if (entry.type === "user" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_result" && content.tool_use_id) { - toolUsePairs.set(content.tool_use_id, content); - } - } - } - } - lines.push("Conversation:"); - lines.push(""); - let conversationLineCount = 0; - const MAX_CONVERSATION_LINES = 50; - let conversationTruncated = false; - for (const entry of logEntries) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (content.type === "text" && content.text) { - const text = content.text.trim(); - if (text && text.length > 0) { - const maxTextLength = 500; - let displayText = text; - if (displayText.length > maxTextLength) { - displayText = displayText.substring(0, maxTextLength) + "..."; - } - const textLines = displayText.split("\n"); - for (const line of textLines) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - lines.push(`Agent: ${line}`); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } else if (content.type === "tool_use") { - const toolName = content.name; - const input = content.input || {}; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - const statusIcon = isError ? "✗" : "✓"; - let displayName; - let resultPreview = ""; - if (toolName === "Bash") { - const cmd = formatBashCommand(input.command || ""); - displayName = `$ ${cmd}`; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const resultLines = resultText.split("\n").filter(l => l.trim()); - if (resultLines.length > 0) { - const previewLine = resultLines[0].substring(0, 80); - if (resultLines.length > 1) { - resultPreview = ` └ ${resultLines.length} lines...`; - } else if (previewLine) { - resultPreview = ` └ ${previewLine}`; - } - } - } - } else if (toolName.startsWith("mcp__")) { - const formattedName = formatMcpName(toolName).replace("::", "-"); - displayName = formattedName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } else { - displayName = toolName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } - lines.push(`${statusIcon} ${displayName}`); - conversationLineCount++; - if (resultPreview) { - lines.push(resultPreview); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } - } - } - if (conversationTruncated) { - lines.push("... (conversation truncated)"); - lines.push(""); - } - const lastEntry = logEntries[logEntries.length - 1]; - lines.push("Statistics:"); - if (lastEntry?.num_turns) { - lines.push(` Turns: ${lastEntry.num_turns}`); - } - if (lastEntry?.duration_ms) { - const duration = formatDuration(lastEntry.duration_ms); - if (duration) { - lines.push(` Duration: ${duration}`); - } - } - let toolCounts = { total: 0, success: 0, error: 0 }; - for (const entry of logEntries) { - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_use") { - const toolName = content.name; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - toolCounts.total++; - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - if (isError) { - toolCounts.error++; - } else { - toolCounts.success++; - } - } - } - } - } - if (toolCounts.total > 0) { - lines.push(` Tools: ${toolCounts.success}/${toolCounts.total} succeeded`); - } - if (lastEntry?.usage) { - const usage = lastEntry.usage; - if (usage.input_tokens || usage.output_tokens) { - const inputTokens = usage.input_tokens || 0; - const outputTokens = usage.output_tokens || 0; - const cacheCreationTokens = usage.cache_creation_input_tokens || 0; - const cacheReadTokens = usage.cache_read_input_tokens || 0; - const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; - lines.push( - ` Tokens: ${totalTokens.toLocaleString()} total (${usage.input_tokens.toLocaleString()} in / ${usage.output_tokens.toLocaleString()} out)` - ); - } - } - if (lastEntry?.total_cost_usd) { - lines.push(` Cost: $${lastEntry.total_cost_usd.toFixed(4)}`); - } - return lines.join("\n"); - } - function generateCopilotCliStyleSummary(logEntries, options = {}) { - const { model, parserName = "Agent" } = options; - const lines = []; - const toolUsePairs = new Map(); - for (const entry of logEntries) { - if (entry.type === "user" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_result" && content.tool_use_id) { - toolUsePairs.set(content.tool_use_id, content); - } - } - } - } - lines.push("```"); - lines.push("Conversation:"); - lines.push(""); - let conversationLineCount = 0; - const MAX_CONVERSATION_LINES = 50; - let conversationTruncated = false; - for (const entry of logEntries) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - if (content.type === "text" && content.text) { - const text = content.text.trim(); - if (text && text.length > 0) { - const maxTextLength = 500; - let displayText = text; - if (displayText.length > maxTextLength) { - displayText = displayText.substring(0, maxTextLength) + "..."; - } - const textLines = displayText.split("\n"); - for (const line of textLines) { - if (conversationLineCount >= MAX_CONVERSATION_LINES) { - conversationTruncated = true; - break; - } - lines.push(`Agent: ${line}`); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } else if (content.type === "tool_use") { - const toolName = content.name; - const input = content.input || {}; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - const statusIcon = isError ? "✗" : "✓"; - let displayName; - let resultPreview = ""; - if (toolName === "Bash") { - const cmd = formatBashCommand(input.command || ""); - displayName = `$ ${cmd}`; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const resultLines = resultText.split("\n").filter(l => l.trim()); - if (resultLines.length > 0) { - const previewLine = resultLines[0].substring(0, 80); - if (resultLines.length > 1) { - resultPreview = ` └ ${resultLines.length} lines...`; - } else if (previewLine) { - resultPreview = ` └ ${previewLine}`; - } - } - } - } else if (toolName.startsWith("mcp__")) { - const formattedName = formatMcpName(toolName).replace("::", "-"); - displayName = formattedName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } else { - displayName = toolName; - if (toolResult && toolResult.content) { - const resultText = typeof toolResult.content === "string" ? toolResult.content : String(toolResult.content); - const truncated = resultText.length > 80 ? resultText.substring(0, 80) + "..." : resultText; - resultPreview = ` └ ${truncated}`; - } - } - lines.push(`${statusIcon} ${displayName}`); - conversationLineCount++; - if (resultPreview) { - lines.push(resultPreview); - conversationLineCount++; - } - lines.push(""); - conversationLineCount++; - } - } - } - } - if (conversationTruncated) { - lines.push("... (conversation truncated)"); - lines.push(""); - } - const lastEntry = logEntries[logEntries.length - 1]; - lines.push("Statistics:"); - if (lastEntry?.num_turns) { - lines.push(` Turns: ${lastEntry.num_turns}`); - } - if (lastEntry?.duration_ms) { - const duration = formatDuration(lastEntry.duration_ms); - if (duration) { - lines.push(` Duration: ${duration}`); - } - } - let toolCounts = { total: 0, success: 0, error: 0 }; - for (const entry of logEntries) { - if (entry.type === "assistant" && entry.message?.content) { - for (const content of entry.message.content) { - if (content.type === "tool_use") { - const toolName = content.name; - if (["Read", "Write", "Edit", "MultiEdit", "LS", "Grep", "Glob", "TodoWrite"].includes(toolName)) { - continue; - } - toolCounts.total++; - const toolResult = toolUsePairs.get(content.id); - const isError = toolResult?.is_error === true; - if (isError) { - toolCounts.error++; - } else { - toolCounts.success++; - } - } - } - } - } - if (toolCounts.total > 0) { - lines.push(` Tools: ${toolCounts.success}/${toolCounts.total} succeeded`); - } - if (lastEntry?.usage) { - const usage = lastEntry.usage; - if (usage.input_tokens || usage.output_tokens) { - const inputTokens = usage.input_tokens || 0; - const outputTokens = usage.output_tokens || 0; - const cacheCreationTokens = usage.cache_creation_input_tokens || 0; - const cacheReadTokens = usage.cache_read_input_tokens || 0; - const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens; - lines.push( - ` Tokens: ${totalTokens.toLocaleString()} total (${usage.input_tokens.toLocaleString()} in / ${usage.output_tokens.toLocaleString()} out)` - ); - } - } - if (lastEntry?.total_cost_usd) { - lines.push(` Cost: $${lastEntry.total_cost_usd.toFixed(4)}`); - } - lines.push("```"); - return lines.join("\n"); - } - function runLogParser(options) { - const fs = require("fs"); - const path = require("path"); - const { parseLog, parserName, supportsDirectories = false } = options; - try { - const logPath = process.env.GH_AW_AGENT_OUTPUT; - if (!logPath) { - core.info("No agent log file specified"); - return; - } - if (!fs.existsSync(logPath)) { - core.info(`Log path not found: ${logPath}`); - return; - } - let content = ""; - const stat = fs.statSync(logPath); - if (stat.isDirectory()) { - if (!supportsDirectories) { - core.info(`Log path is a directory but ${parserName} parser does not support directories: ${logPath}`); - return; - } - const files = fs.readdirSync(logPath); - const logFiles = files.filter(file => file.endsWith(".log") || file.endsWith(".txt")); - if (logFiles.length === 0) { - core.info(`No log files found in directory: ${logPath}`); - return; - } - logFiles.sort(); - for (const file of logFiles) { - const filePath = path.join(logPath, file); - const fileContent = fs.readFileSync(filePath, "utf8"); - if (content.length > 0 && !content.endsWith("\n")) { - content += "\n"; - } - content += fileContent; - } - } else { - content = fs.readFileSync(logPath, "utf8"); - } - const result = parseLog(content); - let markdown = ""; - let mcpFailures = []; - let maxTurnsHit = false; - let logEntries = null; - if (typeof result === "string") { - markdown = result; - } else if (result && typeof result === "object") { - markdown = result.markdown || ""; - mcpFailures = result.mcpFailures || []; - maxTurnsHit = result.maxTurnsHit || false; - logEntries = result.logEntries || null; - } - if (markdown) { - if (logEntries && Array.isArray(logEntries) && logEntries.length > 0) { - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); - const model = initEntry?.model || null; - const plainTextSummary = generatePlainTextSummary(logEntries, { - model, - parserName, - }); - core.info(plainTextSummary); - const copilotCliStyleMarkdown = generateCopilotCliStyleSummary(logEntries, { - model, - parserName, - }); - core.summary.addRaw(copilotCliStyleMarkdown).write(); - } else { - core.info(`${parserName} log parsed successfully`); - core.summary.addRaw(markdown).write(); - } - } else { - core.error(`Failed to parse ${parserName} log`); - } - if (mcpFailures && mcpFailures.length > 0) { - const failedServers = mcpFailures.join(", "); - core.setFailed(`MCP server(s) failed to launch: ${failedServers}`); - } - if (maxTurnsHit) { - core.setFailed(`Agent execution stopped: max-turns limit reached. The agent did not complete its task successfully.`); - } - } catch (error) { - core.setFailed(error instanceof Error ? error : String(error)); - } - } - function main() { - runLogParser({ - parseLog: parseCopilotLog, - parserName: "Copilot", - supportsDirectories: true, - }); - } - function extractPremiumRequestCount(logContent) { - const patterns = [ - /premium\s+requests?\s+consumed:?\s*(\d+)/i, - /(\d+)\s+premium\s+requests?\s+consumed/i, - /consumed\s+(\d+)\s+premium\s+requests?/i, - ]; - for (const pattern of patterns) { - const match = logContent.match(pattern); - if (match && match[1]) { - const count = parseInt(match[1], 10); - if (!isNaN(count) && count > 0) { - return count; - } - } - } - return 1; - } - function parseCopilotLog(logContent) { - try { - let logEntries; - try { - logEntries = JSON.parse(logContent); - if (!Array.isArray(logEntries)) { - throw new Error("Not a JSON array"); - } - } catch (jsonArrayError) { - const debugLogEntries = parseDebugLogFormat(logContent); - if (debugLogEntries && debugLogEntries.length > 0) { - logEntries = debugLogEntries; - } else { - logEntries = parseLogEntries(logContent); - } - } - if (!logEntries || logEntries.length === 0) { - return { markdown: "## Agent Log Summary\n\nLog format not recognized as Copilot JSON array or JSONL.\n", logEntries: [] }; - } - const conversationResult = generateConversationMarkdown(logEntries, { - formatToolCallback: (toolUse, toolResult) => formatToolUse(toolUse, toolResult, { includeDetailedParameters: true }), - formatInitCallback: initEntry => - formatInitializationSummary(initEntry, { - includeSlashCommands: false, - modelInfoCallback: entry => { - if (!entry.model_info) return ""; - const modelInfo = entry.model_info; - let markdown = ""; - if (modelInfo.name) { - markdown += `**Model Name:** ${modelInfo.name}`; - if (modelInfo.vendor) { - markdown += ` (${modelInfo.vendor})`; - } - markdown += "\n\n"; - } - if (modelInfo.billing) { - const billing = modelInfo.billing; - if (billing.is_premium === true) { - markdown += `**Premium Model:** Yes`; - if (billing.multiplier && billing.multiplier !== 1) { - markdown += ` (${billing.multiplier}x cost multiplier)`; - } - markdown += "\n"; - if (billing.restricted_to && Array.isArray(billing.restricted_to) && billing.restricted_to.length > 0) { - markdown += `**Required Plans:** ${billing.restricted_to.join(", ")}\n`; - } - markdown += "\n"; - } else if (billing.is_premium === false) { - markdown += `**Premium Model:** No\n\n`; - } - } - return markdown; - }, - }), - }); - let markdown = conversationResult.markdown; - const lastEntry = logEntries[logEntries.length - 1]; - const initEntry = logEntries.find(entry => entry.type === "system" && entry.subtype === "init"); - markdown += generateInformationSection(lastEntry, { - additionalInfoCallback: entry => { - const isPremiumModel = - initEntry && initEntry.model_info && initEntry.model_info.billing && initEntry.model_info.billing.is_premium === true; - if (isPremiumModel) { - const premiumRequestCount = extractPremiumRequestCount(logContent); - return `**Premium Requests Consumed:** ${premiumRequestCount}\n\n`; - } - return ""; - }, - }); - return { markdown, logEntries }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - return { - markdown: `## Agent Log Summary\n\nError parsing Copilot log (tried both JSON array and JSONL formats): ${errorMessage}\n`, - logEntries: [], - }; - } - } - function scanForToolErrors(logContent) { - const toolErrors = new Map(); - const lines = logContent.split("\n"); - const recentToolCalls = []; - const MAX_RECENT_TOOLS = 10; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes('"tool_calls":') && !line.includes('\\"tool_calls\\"')) { - for (let j = i + 1; j < Math.min(i + 30, lines.length); j++) { - const nextLine = lines[j]; - const idMatch = nextLine.match(/"id":\s*"([^"]+)"/); - const nameMatch = nextLine.match(/"name":\s*"([^"]+)"/) && !nextLine.includes('\\"name\\"'); - if (idMatch) { - const toolId = idMatch[1]; - for (let k = j; k < Math.min(j + 10, lines.length); k++) { - const nameLine = lines[k]; - const funcNameMatch = nameLine.match(/"name":\s*"([^"]+)"/); - if (funcNameMatch && !nameLine.includes('\\"name\\"')) { - const toolName = funcNameMatch[1]; - recentToolCalls.unshift({ id: toolId, name: toolName }); - if (recentToolCalls.length > MAX_RECENT_TOOLS) { - recentToolCalls.pop(); - } - break; - } - } - } - } - } - const errorMatch = line.match(/\[ERROR\].*(?:Tool execution failed|Permission denied|Resource not accessible|Error executing tool)/i); - if (errorMatch) { - const toolNameMatch = line.match(/Tool execution failed:\s*([^\s]+)/i); - const toolIdMatch = line.match(/tool_call_id:\s*([^\s]+)/i); - if (toolNameMatch) { - const toolName = toolNameMatch[1]; - toolErrors.set(toolName, true); - const matchingTool = recentToolCalls.find(t => t.name === toolName); - if (matchingTool) { - toolErrors.set(matchingTool.id, true); - } - } else if (toolIdMatch) { - toolErrors.set(toolIdMatch[1], true); - } else if (recentToolCalls.length > 0) { - const lastTool = recentToolCalls[0]; - toolErrors.set(lastTool.id, true); - toolErrors.set(lastTool.name, true); - } - } - } - return toolErrors; - } - function parseDebugLogFormat(logContent) { - const entries = []; - const lines = logContent.split("\n"); - const toolErrors = scanForToolErrors(logContent); - let model = "unknown"; - let sessionId = null; - let modelInfo = null; - let tools = []; - const modelMatch = logContent.match(/Starting Copilot CLI: ([\d.]+)/); - if (modelMatch) { - sessionId = `copilot-${modelMatch[1]}-${Date.now()}`; - } - const gotModelInfoIndex = logContent.indexOf("[DEBUG] Got model info: {"); - if (gotModelInfoIndex !== -1) { - const jsonStart = logContent.indexOf("{", gotModelInfoIndex); - if (jsonStart !== -1) { - let braceCount = 0; - let inString = false; - let escapeNext = false; - let jsonEnd = -1; - for (let i = jsonStart; i < logContent.length; i++) { - const char = logContent[i]; - if (escapeNext) { - escapeNext = false; - continue; - } - if (char === "\\") { - escapeNext = true; - continue; - } - if (char === '"' && !escapeNext) { - inString = !inString; - continue; - } - if (inString) continue; - if (char === "{") { - braceCount++; - } else if (char === "}") { - braceCount--; - if (braceCount === 0) { - jsonEnd = i + 1; - break; - } - } - } - if (jsonEnd !== -1) { - const modelInfoJson = logContent.substring(jsonStart, jsonEnd); - try { - modelInfo = JSON.parse(modelInfoJson); - } catch (e) { - } - } - } - } - const toolsIndex = logContent.indexOf("[DEBUG] Tools:"); - if (toolsIndex !== -1) { - const afterToolsLine = logContent.indexOf("\n", toolsIndex); - let toolsStart = logContent.indexOf("[DEBUG] [", afterToolsLine); - if (toolsStart !== -1) { - toolsStart = logContent.indexOf("[", toolsStart + 7); - } - if (toolsStart !== -1) { - let bracketCount = 0; - let inString = false; - let escapeNext = false; - let toolsEnd = -1; - for (let i = toolsStart; i < logContent.length; i++) { - const char = logContent[i]; - if (escapeNext) { - escapeNext = false; - continue; - } - if (char === "\\") { - escapeNext = true; - continue; - } - if (char === '"' && !escapeNext) { - inString = !inString; - continue; - } - if (inString) continue; - if (char === "[") { - bracketCount++; - } else if (char === "]") { - bracketCount--; - if (bracketCount === 0) { - toolsEnd = i + 1; - break; - } - } - } - if (toolsEnd !== -1) { - let toolsJson = logContent.substring(toolsStart, toolsEnd); - toolsJson = toolsJson.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /gm, ""); - try { - const toolsArray = JSON.parse(toolsJson); - if (Array.isArray(toolsArray)) { - tools = toolsArray - .map(tool => { - if (tool.type === "function" && tool.function && tool.function.name) { - let name = tool.function.name; - if (name.startsWith("github-")) { - name = "mcp__github__" + name.substring(7); - } else if (name.startsWith("safe_outputs-")) { - name = name; - } - return name; - } - return null; - }) - .filter(name => name !== null); - } - } catch (e) { - } - } - } - } - let inDataBlock = false; - let currentJsonLines = []; - let turnCount = 0; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (line.includes("[DEBUG] data:")) { - inDataBlock = true; - currentJsonLines = []; - continue; - } - if (inDataBlock) { - const hasTimestamp = line.match(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z /); - if (hasTimestamp) { - const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); - const isJsonContent = /^[{\[}\]"]/.test(cleanLine) || cleanLine.trim().startsWith('"'); - if (!isJsonContent) { - if (currentJsonLines.length > 0) { - try { - const jsonStr = currentJsonLines.join("\n"); - const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - const originalToolName = toolName; - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; - } - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - const hasError = toolErrors.has(toolId) || toolErrors.has(originalToolName); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: hasError ? "Permission denied or tool execution failed" : "", - is_error: hasError, - }); - } - } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { - entries.push({ - type: "user", - message: { content: toolResults }, - }); - } - } - } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, - }; - } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; - } - } - } catch (e) { - } - } - inDataBlock = false; - currentJsonLines = []; - continue; - } else if (hasTimestamp && isJsonContent) { - currentJsonLines.push(cleanLine); - } - } else { - const cleanLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z \[DEBUG\] /, ""); - currentJsonLines.push(cleanLine); - } - } - } - if (inDataBlock && currentJsonLines.length > 0) { - try { - const jsonStr = currentJsonLines.join("\n"); - const jsonData = JSON.parse(jsonStr); - if (jsonData.model) { - model = jsonData.model; - } - if (jsonData.choices && Array.isArray(jsonData.choices)) { - for (const choice of jsonData.choices) { - if (choice.message) { - const message = choice.message; - const content = []; - const toolResults = []; - if (message.content && message.content.trim()) { - content.push({ - type: "text", - text: message.content, - }); - } - if (message.tool_calls && Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (toolCall.function) { - let toolName = toolCall.function.name; - const originalToolName = toolName; - const toolId = toolCall.id || `tool_${Date.now()}_${Math.random()}`; - let args = {}; - if (toolName.startsWith("github-")) { - toolName = "mcp__github__" + toolName.substring(7); - } else if (toolName === "bash") { - toolName = "Bash"; - } - try { - args = JSON.parse(toolCall.function.arguments); - } catch (e) { - args = {}; - } - content.push({ - type: "tool_use", - id: toolId, - name: toolName, - input: args, - }); - const hasError = toolErrors.has(toolId) || toolErrors.has(originalToolName); - toolResults.push({ - type: "tool_result", - tool_use_id: toolId, - content: hasError ? "Permission denied or tool execution failed" : "", - is_error: hasError, - }); - } - } - } - if (content.length > 0) { - entries.push({ - type: "assistant", - message: { content }, - }); - turnCount++; - if (toolResults.length > 0) { - entries.push({ - type: "user", - message: { content: toolResults }, - }); - } - } - } - } - if (jsonData.usage) { - if (!entries._accumulatedUsage) { - entries._accumulatedUsage = { - input_tokens: 0, - output_tokens: 0, - }; - } - if (jsonData.usage.prompt_tokens) { - entries._accumulatedUsage.input_tokens += jsonData.usage.prompt_tokens; - } - if (jsonData.usage.completion_tokens) { - entries._accumulatedUsage.output_tokens += jsonData.usage.completion_tokens; - } - entries._lastResult = { - type: "result", - num_turns: turnCount, - usage: entries._accumulatedUsage, - }; - } - } - } catch (e) { - } - } - if (entries.length > 0) { - const initEntry = { - type: "system", - subtype: "init", - session_id: sessionId, - model: model, - tools: tools, - }; - if (modelInfo) { - initEntry.model_info = modelInfo; - } - entries.unshift(initEntry); - if (entries._lastResult) { - entries.push(entries._lastResult); - delete entries._lastResult; - } - } - return entries; - } - main(); - - name: Upload Agent Stdio - if: always() - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 - with: - name: agent-stdio.log - path: /tmp/gh-aw/agent-stdio.log - if-no-files-found: warn - - name: Validate agent logs for errors - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - GH_AW_ERROR_PATTERNS: "[{\"id\":\"\",\"pattern\":\"::(error)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - error\"},{\"id\":\"\",\"pattern\":\"::(warning)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - warning\"},{\"id\":\"\",\"pattern\":\"::(notice)(?:\\\\s+[^:]*)?::(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"GitHub Actions workflow command - notice\"},{\"id\":\"\",\"pattern\":\"(ERROR|Error):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Generic ERROR messages\"},{\"id\":\"\",\"pattern\":\"(WARNING|Warning):\\\\s+(.+)\",\"level_group\":1,\"message_group\":2,\"description\":\"Generic WARNING messages\"},{\"id\":\"\",\"pattern\":\"(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\s+\\\\[(ERROR)\\\\]\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI timestamped ERROR messages\"},{\"id\":\"\",\"pattern\":\"(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\s+\\\\[(WARN|WARNING)\\\\]\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI timestamped WARNING messages\"},{\"id\":\"\",\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\]\\\\s+(CRITICAL|ERROR):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI bracketed critical/error messages with timestamp\"},{\"id\":\"\",\"pattern\":\"\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d{3}Z)\\\\]\\\\s+(WARNING):\\\\s+(.+)\",\"level_group\":2,\"message_group\":3,\"description\":\"Copilot CLI bracketed warning messages with timestamp\"},{\"id\":\"\",\"pattern\":\"✗\\\\s+(.+)\",\"level_group\":0,\"message_group\":1,\"description\":\"Copilot CLI failed command indicator\"},{\"id\":\"\",\"pattern\":\"(?:command not found|not found):\\\\s*(.+)|(.+):\\\\s*(?:command not found|not found)\",\"level_group\":0,\"message_group\":0,\"description\":\"Shell command not found error\"},{\"id\":\"\",\"pattern\":\"Cannot find module\\\\s+['\\\"](.+)['\\\"]\",\"level_group\":0,\"message_group\":1,\"description\":\"Node.js module not found error\"},{\"id\":\"\",\"pattern\":\"Permission denied and could not request permission from user\",\"level_group\":0,\"message_group\":0,\"description\":\"Copilot CLI permission denied warning (user interaction required)\"},{\"id\":\"\",\"pattern\":\"\\\\berror\\\\b.*permission.*denied\",\"level_group\":0,\"message_group\":0,\"description\":\"Permission denied error (requires error context)\"},{\"id\":\"\",\"pattern\":\"\\\\berror\\\\b.*unauthorized\",\"level_group\":0,\"message_group\":0,\"description\":\"Unauthorized access error (requires error context)\"},{\"id\":\"\",\"pattern\":\"\\\\berror\\\\b.*forbidden\",\"level_group\":0,\"message_group\":0,\"description\":\"Forbidden access error (requires error context)\"}]" - with: - script: | - function main() { - const fs = require("fs"); - const path = require("path"); - core.info("Starting validate_errors.cjs script"); - const startTime = Date.now(); - try { - const logPath = process.env.GH_AW_AGENT_OUTPUT; - if (!logPath) { - throw new Error("GH_AW_AGENT_OUTPUT environment variable is required"); - } - core.info(`Log path: ${logPath}`); - if (!fs.existsSync(logPath)) { - core.info(`Log path not found: ${logPath}`); - core.info("No logs to validate - skipping error validation"); - return; - } - const patterns = getErrorPatternsFromEnv(); - if (patterns.length === 0) { - throw new Error("GH_AW_ERROR_PATTERNS environment variable is required and must contain at least one pattern"); - } - core.info(`Loaded ${patterns.length} error patterns`); - core.info(`Patterns: ${JSON.stringify(patterns.map(p => ({ description: p.description, pattern: p.pattern })))}`); - let content = ""; - const stat = fs.statSync(logPath); - if (stat.isDirectory()) { - const files = fs.readdirSync(logPath); - const logFiles = files.filter(file => file.endsWith(".log") || file.endsWith(".txt")); - if (logFiles.length === 0) { - core.info(`No log files found in directory: ${logPath}`); - return; - } - core.info(`Found ${logFiles.length} log files in directory`); - logFiles.sort(); - for (const file of logFiles) { - const filePath = path.join(logPath, file); - const fileContent = fs.readFileSync(filePath, "utf8"); - core.info(`Reading log file: ${file} (${fileContent.length} bytes)`); - content += fileContent; - if (content.length > 0 && !content.endsWith("\n")) { - content += "\n"; - } - } - } else { - content = fs.readFileSync(logPath, "utf8"); - core.info(`Read single log file (${content.length} bytes)`); - } - core.info(`Total log content size: ${content.length} bytes, ${content.split("\n").length} lines`); - const hasErrors = validateErrors(content, patterns); - const elapsedTime = Date.now() - startTime; - core.info(`Error validation completed in ${elapsedTime}ms`); - if (hasErrors) { - core.error("Errors detected in agent logs - continuing workflow step (not failing for now)"); - } else { - core.info("Error validation completed successfully"); - } - } catch (error) { - console.debug(error); - core.error(`Error validating log: ${error instanceof Error ? error.message : String(error)}`); - } - } - function getErrorPatternsFromEnv() { - const patternsEnv = process.env.GH_AW_ERROR_PATTERNS; - if (!patternsEnv) { - throw new Error("GH_AW_ERROR_PATTERNS environment variable is required"); - } - try { - const patterns = JSON.parse(patternsEnv); - if (!Array.isArray(patterns)) { - throw new Error("GH_AW_ERROR_PATTERNS must be a JSON array"); - } - return patterns; - } catch (e) { - throw new Error(`Failed to parse GH_AW_ERROR_PATTERNS as JSON: ${e instanceof Error ? e.message : String(e)}`); - } - } - function shouldSkipLine(line) { - const GITHUB_ACTIONS_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\s+/; - if (new RegExp(GITHUB_ACTIONS_TIMESTAMP.source + "GH_AW_ERROR_PATTERNS:").test(line)) { - return true; - } - if (/^\s+GH_AW_ERROR_PATTERNS:\s*\[/.test(line)) { - return true; - } - if (new RegExp(GITHUB_ACTIONS_TIMESTAMP.source + "env:").test(line)) { - return true; - } - return false; - } - function validateErrors(logContent, patterns) { - const lines = logContent.split("\n"); - let hasErrors = false; - const MAX_ITERATIONS_PER_LINE = 10000; - const ITERATION_WARNING_THRESHOLD = 1000; - const MAX_TOTAL_ERRORS = 100; - const MAX_LINE_LENGTH = 10000; - const TOP_SLOW_PATTERNS_COUNT = 5; - core.info(`Starting error validation with ${patterns.length} patterns and ${lines.length} lines`); - const validationStartTime = Date.now(); - let totalMatches = 0; - let patternStats = []; - for (let patternIndex = 0; patternIndex < patterns.length; patternIndex++) { - const pattern = patterns[patternIndex]; - const patternStartTime = Date.now(); - let patternMatches = 0; - let regex; - try { - regex = new RegExp(pattern.pattern, "g"); - core.info(`Pattern ${patternIndex + 1}/${patterns.length}: ${pattern.description || "Unknown"} - regex: ${pattern.pattern}`); - } catch (e) { - core.error(`invalid error regex pattern: ${pattern.pattern}`); - continue; - } - for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { - const line = lines[lineIndex]; - if (shouldSkipLine(line)) { - continue; - } - if (line.length > MAX_LINE_LENGTH) { - continue; - } - if (totalMatches >= MAX_TOTAL_ERRORS) { - core.warning(`Stopping error validation after finding ${totalMatches} matches (max: ${MAX_TOTAL_ERRORS})`); - break; - } - let match; - let iterationCount = 0; - let lastIndex = -1; - while ((match = regex.exec(line)) !== null) { - iterationCount++; - if (regex.lastIndex === lastIndex) { - core.error(`Infinite loop detected at line ${lineIndex + 1}! Pattern: ${pattern.pattern}, lastIndex stuck at ${lastIndex}`); - core.error(`Line content (truncated): ${truncateString(line, 200)}`); - break; - } - lastIndex = regex.lastIndex; - if (iterationCount === ITERATION_WARNING_THRESHOLD) { - core.warning( - `High iteration count (${iterationCount}) on line ${lineIndex + 1} with pattern: ${pattern.description || pattern.pattern}` - ); - core.warning(`Line content (truncated): ${truncateString(line, 200)}`); - } - if (iterationCount > MAX_ITERATIONS_PER_LINE) { - core.error(`Maximum iteration limit (${MAX_ITERATIONS_PER_LINE}) exceeded at line ${lineIndex + 1}! Pattern: ${pattern.pattern}`); - core.error(`Line content (truncated): ${truncateString(line, 200)}`); - core.error(`This likely indicates a problematic regex pattern. Skipping remaining matches on this line.`); - break; - } - 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); - } - patternMatches++; - totalMatches++; - } - if (iterationCount > 100) { - core.info(`Line ${lineIndex + 1} had ${iterationCount} matches for pattern: ${pattern.description || pattern.pattern}`); - } - } - const patternElapsed = Date.now() - patternStartTime; - patternStats.push({ - description: pattern.description || "Unknown", - pattern: pattern.pattern.substring(0, 50) + (pattern.pattern.length > 50 ? "..." : ""), - matches: patternMatches, - timeMs: patternElapsed, - }); - if (patternElapsed > 5000) { - core.warning(`Pattern "${pattern.description}" took ${patternElapsed}ms to process (${patternMatches} matches)`); - } - if (totalMatches >= MAX_TOTAL_ERRORS) { - core.warning(`Stopping pattern processing after finding ${totalMatches} matches (max: ${MAX_TOTAL_ERRORS})`); - break; - } - } - const validationElapsed = Date.now() - validationStartTime; - core.info(`Validation summary: ${totalMatches} total matches found in ${validationElapsed}ms`); - patternStats.sort((a, b) => b.timeMs - a.timeMs); - const topSlow = patternStats.slice(0, TOP_SLOW_PATTERNS_COUNT); - if (topSlow.length > 0 && topSlow[0].timeMs > 1000) { - core.info(`Top ${TOP_SLOW_PATTERNS_COUNT} slowest patterns:`); - topSlow.forEach((stat, idx) => { - core.info(` ${idx + 1}. "${stat.description}" - ${stat.timeMs}ms (${stat.matches} matches)`); - }); - } - core.info(`Error validation completed. Errors found: ${hasErrors}`); - return hasErrors; - } - function extractLevel(match, pattern) { - if (pattern.level_group && pattern.level_group > 0 && match[pattern.level_group]) { - return match[pattern.level_group]; - } - const fullMatch = match[0]; - if (fullMatch.toLowerCase().includes("error")) { - return "error"; - } else if (fullMatch.toLowerCase().includes("warn")) { - return "warning"; - } - return "unknown"; - } - function extractMessage(match, pattern, fullLine) { - if (pattern.message_group && pattern.message_group > 0 && match[pattern.message_group]) { - return match[pattern.message_group].trim(); - } - return match[0] || fullLine.trim(); - } - function truncateString(str, maxLength) { - if (!str) return ""; - if (str.length <= maxLength) return str; - return str.substring(0, maxLength) + "..."; - } - if (typeof module !== "undefined" && module.exports) { - module.exports = { - validateErrors, - extractLevel, - extractMessage, - getErrorPatternsFromEnv, - truncateString, - shouldSkipLine, - }; - } - if (typeof module === "undefined" || require.main === module) { - main(); - } - - pre_activation: - runs-on: ubuntu-slim - outputs: - activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} - steps: - - name: Check team membership for workflow - id: check_membership - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_REQUIRED_ROLES: - with: - script: | - function parseRequiredPermissions() { - const requiredPermissionsEnv = process.env.GH_AW_REQUIRED_ROLES; - return requiredPermissionsEnv ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") : []; - } - function parseAllowedBots() { - const allowedBotsEnv = process.env.GH_AW_ALLOWED_BOTS; - return allowedBotsEnv ? allowedBotsEnv.split(",").filter(b => b.trim() !== "") : []; - } - async function checkBotStatus(actor, owner, repo) { - try { - const isBot = actor.endsWith("[bot]"); - if (!isBot) { - return { isBot: false, isActive: false }; - } - core.info(`Checking if bot '${actor}' is active on ${owner}/${repo}`); - try { - const botPermission = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - core.info(`Bot '${actor}' is active with permission level: ${botPermission.data.permission}`); - return { isBot: true, isActive: true }; - } catch (botError) { - if (typeof botError === "object" && botError !== null && "status" in botError && botError.status === 404) { - core.warning(`Bot '${actor}' is not active/installed on ${owner}/${repo}`); - return { isBot: true, isActive: false }; - } - const errorMessage = botError instanceof Error ? botError.message : String(botError); - core.warning(`Failed to check bot status: ${errorMessage}`); - return { isBot: true, isActive: false, error: errorMessage }; - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - core.warning(`Error checking bot status: ${errorMessage}`); - return { isBot: false, isActive: false, error: errorMessage }; - } - } - async function checkRepositoryPermission(actor, owner, repo, requiredPermissions) { - try { - core.info(`Checking if user '${actor}' has required permissions for ${owner}/${repo}`); - core.info(`Required permissions: ${requiredPermissions.join(", ")}`); - const repoPermission = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: owner, - repo: repo, - username: actor, - }); - const permission = repoPermission.data.permission; - core.info(`Repository permission level: ${permission}`); - for (const requiredPerm of requiredPermissions) { - if (permission === requiredPerm || (requiredPerm === "maintainer" && permission === "maintain")) { - core.info(`✅ User has ${permission} access to repository`); - return { authorized: true, permission: permission }; - } - } - core.warning(`User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}`); - return { authorized: false, permission: permission }; - } catch (repoError) { - const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); - core.warning(`Repository permission check failed: ${errorMessage}`); - return { authorized: false, error: errorMessage }; - } - } - async function main() { - const { eventName } = context; - const actor = context.actor; - const { owner, repo } = context.repo; - const requiredPermissions = parseRequiredPermissions(); - const allowedBots = parseAllowedBots(); - if (eventName === "workflow_dispatch") { - const hasWriteRole = requiredPermissions.includes("write"); - if (hasWriteRole) { - core.info(`✅ Event ${eventName} does not require validation (write role allowed)`); - core.setOutput("is_team_member", "true"); - core.setOutput("result", "safe_event"); - return; - } - core.info(`Event ${eventName} requires validation (write role not allowed)`); - } - const safeEvents = ["schedule"]; - if (safeEvents.includes(eventName)) { - core.info(`✅ Event ${eventName} does not require validation`); - core.setOutput("is_team_member", "true"); - core.setOutput("result", "safe_event"); - return; - } - if (!requiredPermissions || requiredPermissions.length === 0) { - core.warning("❌ Configuration error: Required permissions not specified. Contact repository administrator."); - core.setOutput("is_team_member", "false"); - core.setOutput("result", "config_error"); - core.setOutput("error_message", "Configuration error: Required permissions not specified"); - return; - } - const result = await checkRepositoryPermission(actor, owner, repo, requiredPermissions); - if (result.error) { - core.setOutput("is_team_member", "false"); - core.setOutput("result", "api_error"); - core.setOutput("error_message", `Repository permission check failed: ${result.error}`); - return; - } - if (result.authorized) { - core.setOutput("is_team_member", "true"); - core.setOutput("result", "authorized"); - core.setOutput("user_permission", result.permission); - } else { - if (allowedBots && allowedBots.length > 0) { - core.info(`Checking if actor '${actor}' is in allowed bots list: ${allowedBots.join(", ")}`); - if (allowedBots.includes(actor)) { - core.info(`Actor '${actor}' is in the allowed bots list`); - const botStatus = await checkBotStatus(actor, owner, repo); - if (botStatus.isBot && botStatus.isActive) { - core.info(`✅ Bot '${actor}' is active on the repository and authorized`); - core.setOutput("is_team_member", "true"); - core.setOutput("result", "authorized_bot"); - core.setOutput("user_permission", "bot"); - return; - } else if (botStatus.isBot && !botStatus.isActive) { - core.warning(`Bot '${actor}' is in the allowed list but not active/installed on ${owner}/${repo}`); - core.setOutput("is_team_member", "false"); - core.setOutput("result", "bot_not_active"); - core.setOutput("user_permission", result.permission); - core.setOutput("error_message", `Access denied: Bot '${actor}' is not active/installed on this repository`); - return; - } else { - core.info(`Actor '${actor}' is in allowed bots list but bot status check failed`); - } - } - } - core.setOutput("is_team_member", "false"); - core.setOutput("result", "insufficient_permissions"); - core.setOutput("user_permission", result.permission); - core.setOutput( - "error_message", - `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` - ); - } - } - await main(); - diff --git a/.github/workflows/security-compliance.campaign.g.lock.yml b/.github/workflows/security-compliance.campaign.g.lock.yml index 8dabab1f2ee..fe2aedfe8e9 100644 --- a/.github/workflows/security-compliance.campaign.g.lock.yml +++ b/.github/workflows/security-compliance.campaign.g.lock.yml @@ -2270,6 +2270,7 @@ jobs: env: GH_AW_REQUIRED_ROLES: admin,maintainer,write with: + github-token: ${{ secrets.GITHUB_TOKEN }} script: | function parseRequiredPermissions() { const requiredPermissionsEnv = process.env.GH_AW_REQUIRED_ROLES; diff --git a/go.mod b/go.mod index ca8fc4f1bbc..ff1f7b159af 100644 --- a/go.mod +++ b/go.mod @@ -14,12 +14,12 @@ require ( github.com/modelcontextprotocol/go-sdk v1.1.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/sourcegraph/conc v0.3.0 - github.com/spf13/cobra v1.10.2 - github.com/stretchr/testify v1.11.1 - github.com/xeipuuv/gojsonschema v1.2.0 - golang.org/x/term v0.38.0 - gopkg.in/yaml.v3 v3.0.1 - ) + github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.11.1 + github.com/xeipuuv/gojsonschema v1.2.0 + golang.org/x/term v0.38.0 + gopkg.in/yaml.v3 v3.0.1 +) require ( github.com/atotto/clipboard v0.1.4 // indirect diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json index c96dafdf0af..34b4e7bbb00 100644 --- a/pkg/workflow/data/action_pins.json +++ b/pkg/workflow/data/action_pins.json @@ -113,7 +113,7 @@ "github/codeql-action/upload-sarif@v3": { "repo": "github/codeql-action/upload-sarif", "version": "v3", - "sha": "6d10af73d5896080e7b530ea0822d927c05661f1" + "sha": "323fb8c0ad5be63b7a6ebf1f32c35882fcfea2cf" }, "github/stale-repos@v3": { "repo": "github/stale-repos",