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",