diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index 3fc2d5d190..e2483cf0fd 100644 --- a/.github/workflows/issue-triage.lock.yml +++ b/.github/workflows/issue-triage.lock.yml @@ -5,7 +5,7 @@ # # Source: githubnext/agentics/workflows/issue-triage.md@0837fb7b24c3b84ee77fb7c8cfa8735c48be347a # -# Effective stop-time: 2025-12-03 20:01:19 +# Effective stop-time: 2025-12-12 19:43:36 # # Job Dependency Graph: # ```mermaid @@ -43,8 +43,8 @@ # https://github.com/actions/github-script/commit/ed597411d8f924073f98dfc5c65a23a2325f34cd # - actions/setup-node@v6 (2028fbc5c25fe9cf00d9f06a71cc4710d4507903) # https://github.com/actions/setup-node/commit/2028fbc5c25fe9cf00d9f06a71cc4710d4507903 -# - actions/upload-artifact@v4 (ea165f8d65b6e75b540449e92b4886f43607fa02) -# https://github.com/actions/upload-artifact/commit/ea165f8d65b6e75b540449e92b4886f43607fa02 +# - actions/upload-artifact@v5 (330a01c490aca151604b8cf639adc76d48f6c5d4) +# https://github.com/actions/upload-artifact/commit/330a01c490aca151604b8cf639adc76d48f6c5d4 name: "Agentic Triage" "on": @@ -59,12 +59,22 @@ concurrency: run-name: "Agentic Triage" +env: + GH_AW_ERROR_PATTERNS: |- + [ + {"id":"gh-action-error","pattern":"^::(error)(?:\\\\s+[^:]*)?::(.+)","level_group":1,"message_group":2,"description":"GitHub Actions workflow command - error"}, + {"id":"gh-action-warning","pattern":"^::(warning)(?:\\\\s+[^:]*)?::(.+)","level_group":1,"message_group":2,"description":"GitHub Actions workflow command - warning"}, + {"id":"bracketed-level","pattern":"^\\[(ERROR|CRITICAL|WARNING|WARN)\\]\\s+(.+)","level_group":1,"message_group":2,"description":"Bracketed log level at start of line"}, + {"id":"timestamped-copilot","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z\\s+\\[(ERROR|WARN|WARNING|CRITICAL)\\]\\s+(.+)","level_group":1,"message_group":3,"description":"Timestamped Copilot CLI messages"} + ] + jobs: activation: needs: pre_activation if: needs.pre_activation.outputs.activated == 'true' runs-on: ubuntu-slim permissions: + contents: read discussions: write issues: write pull-requests: write @@ -132,14 +142,22 @@ jobs: core.info(` Source modified: ${workflowStat.mtime.toISOString()}`); core.info(` Lock modified: ${lockStat.mtime.toISOString()}`); if (workflowMtime > lockMtime) { - const warningMessage = `πŸ”΄πŸ”΄πŸ”΄ WARNING: Lock file '${lockFile}' is outdated! The workflow file '${workflowMdFile}' has been modified more recently. Run 'gh aw compile' to regenerate the lock file.`; + const warningMessage = `WARNING: Lock file '${lockFile}' is outdated! The workflow file '${workflowMdFile}' has been modified more recently. Run 'gh aw compile' to regenerate the lock file.`; core.error(warningMessage); - await core.summary - .addRaw("## ⚠️ Workflow Lock File Warning\n\n") - .addRaw(`πŸ”΄πŸ”΄πŸ”΄ **WARNING**: Lock file \`${lockFile}\` is outdated!\n\n`) - .addRaw(`The workflow file \`${workflowMdFile}\` has been modified more recently.\n\n`) - .addRaw("Run `gh aw compile` to regenerate the lock file.\n\n") - .write(); + const workflowTimestamp = workflowStat.mtime.toISOString(); + const lockTimestamp = lockStat.mtime.toISOString(); + const gitSha = process.env.GITHUB_SHA; + 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: \`${workflowMdFile}\` (modified: ${workflowTimestamp})\n`) + .addRaw(`- Lock: \`${lockFile}\` (modified: ${lockTimestamp})\n\n`); + if (gitSha) { + summary = summary.addRaw(`**Git Commit:** \`${gitSha}\`\n\n`); + } + summary = summary.addRaw("**Action Required:** Run `gh aw compile` to regenerate the lock file.\n\n"); + await summary.write(); } else { core.info("βœ… Lock file is up to date"); } @@ -482,9 +500,7 @@ jobs: needs: - agent - detection - if: > - (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'add_comment'))) && - (((github.event.issue.number) || (github.event.pull_request.number)) || (github.event.discussion.number)) + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'add_comment')) runs-on: ubuntu-slim permissions: contents: read @@ -512,8 +528,8 @@ jobs: - name: Setup agent output environment variable run: | mkdir -p /tmp/gh-aw/safeoutputs/ - find /tmp/gh-aw/safeoutputs/ -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> $GITHUB_ENV + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - name: Add Issue Comment id: add_comment uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd @@ -522,9 +538,44 @@ jobs: GH_AW_WORKFLOW_NAME: "Agentic Triage" GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/issue-triage.md@0837fb7b24c3b84ee77fb7c8cfa8735c48be347a" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/0837fb7b24c3b84ee77fb7c8cfa8735c48be347a/workflows/issue-triage.md" + GH_AW_COMMENT_TARGET: "*" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | + const fs = require("fs"); + function loadAgentOutput() { + const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; + if (!agentOutputFile) { + core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + return { success: false }; + } + let outputContent; + try { + outputContent = fs.readFileSync(agentOutputFile, "utf8"); + } catch (error) { + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.setFailed(errorMessage); + return { success: false, error: errorMessage }; + } + if (outputContent.trim() === "") { + core.info("Agent output content is empty"); + return { success: false }; + } + core.info(`Agent output content length: ${outputContent.length}`); + let validatedOutput; + try { + validatedOutput = JSON.parse(outputContent); + } catch (error) { + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.setFailed(errorMessage); + return { success: false, error: errorMessage }; + } + if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } function generateFooter( workflowName, runUrl, @@ -608,35 +659,11 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; - const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; - if (!agentOutputFile) { - core.info("No GH_AW_AGENT_OUTPUT environment variable found"); + const result = loadAgentOutput(); + if (!result.success) { return; } - let outputContent; - try { - outputContent = require("fs").readFileSync(agentOutputFile, "utf8"); - } catch (error) { - core.setFailed(`Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`); - return; - } - if (outputContent.trim() === "") { - core.info("Agent output content is empty"); - return; - } - core.info(`Agent output content length: ${outputContent.length}`); - let validatedOutput; - try { - validatedOutput = JSON.parse(outputContent); - } catch (error) { - core.setFailed(`Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`); - return; - } - if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { - core.info("No valid items found in agent output"); - return; - } - const commentItems = validatedOutput.items.filter( item => item.type === "add_comment"); + const commentItems = result.items.filter( item => item.type === "add_comment"); if (commentItems.length === 0) { core.info("No add-comment items found in agent output"); return; @@ -873,9 +900,7 @@ jobs: needs: - agent - detection - if: > - (((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'add_labels'))) && - ((github.event.issue.number) || (github.event.pull_request.number)) + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'add_labels')) runs-on: ubuntu-slim permissions: contents: read @@ -894,15 +919,16 @@ jobs: - name: Setup agent output environment variable run: | mkdir -p /tmp/gh-aw/safeoutputs/ - find /tmp/gh-aw/safeoutputs/ -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> $GITHUB_ENV + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - name: Add Labels id: add_labels uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_LABELS_ALLOWED: "" - GH_AW_LABELS_MAX_COUNT: 5 + GH_AW_LABELS_MAX_COUNT: 100 + GH_AW_LABELS_TARGET: "*" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -911,8 +937,8 @@ jobs: return ""; } let sanitized = content.trim(); - sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace(/\x1b\[[0-9;]*[mGKH]/g, ""); + sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ""); sanitized = sanitized.replace( /(^|[^\w`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?(?:\/[A-Za-z0-9._-]+)?)/g, (_m, p1, p2) => `${p1}\`@${p2}\`` @@ -920,54 +946,86 @@ jobs: sanitized = sanitized.replace(/[<>&'"]/g, ""); return sanitized.trim(); } - async function main() { + const fs = require("fs"); + function loadAgentOutput() { const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; if (!agentOutputFile) { core.info("No GH_AW_AGENT_OUTPUT environment variable found"); - return; + return { success: false }; } let outputContent; try { - outputContent = require("fs").readFileSync(agentOutputFile, "utf8"); + outputContent = fs.readFileSync(agentOutputFile, "utf8"); } catch (error) { - core.setFailed(`Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`); - return; + const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; + core.setFailed(errorMessage); + return { success: false, error: errorMessage }; } if (outputContent.trim() === "") { core.info("Agent output content is empty"); - return; + return { success: false }; } core.info(`Agent output content length: ${outputContent.length}`); let validatedOutput; try { validatedOutput = JSON.parse(outputContent); } catch (error) { - core.setFailed(`Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`); - return; + const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; + core.setFailed(errorMessage); + return { success: false, error: errorMessage }; } if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { - core.warning("No valid items found in agent output"); + core.info("No valid items found in agent output"); + return { success: false }; + } + return { success: true, items: validatedOutput.items }; + } + async function generateStagedPreview(options) { + const { title, description, items, renderItem } = options; + let summaryContent = `## 🎭 Staged Mode: ${title} Preview\n\n`; + summaryContent += `${description}\n\n`; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + summaryContent += renderItem(item, i); + summaryContent += "---\n\n"; + } + try { + await core.summary.addRaw(summaryContent).write(); + core.info(summaryContent); + core.info(`πŸ“ ${title} preview written to step summary`); + } catch (error) { + core.setFailed(error instanceof Error ? error : String(error)); + } + } + async function main() { + const result = loadAgentOutput(); + if (!result.success) { return; } - const labelsItem = validatedOutput.items.find(item => item.type === "add_labels"); + const labelsItem = result.items.find(item => item.type === "add_labels"); if (!labelsItem) { core.warning("No add-labels item found in agent output"); return; } core.info(`Found add-labels item with ${labelsItem.labels.length} labels`); if (process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true") { - let summaryContent = "## 🎭 Staged Mode: Add Labels Preview\n\n"; - summaryContent += "The following labels would be added if staged mode was disabled:\n\n"; - if (labelsItem.item_number) { - summaryContent += `**Target Issue:** #${labelsItem.item_number}\n\n`; - } else { - summaryContent += `**Target:** Current issue/PR\n\n`; - } - if (labelsItem.labels && labelsItem.labels.length > 0) { - summaryContent += `**Labels to add:** ${labelsItem.labels.join(", ")}\n\n`; - } - await core.summary.addRaw(summaryContent).write(); - core.info("πŸ“ Label addition preview written to step summary"); + await generateStagedPreview({ + title: "Add Labels", + description: "The following labels would be added if staged mode was disabled:", + items: [labelsItem], + renderItem: item => { + let content = ""; + if (item.item_number) { + content += `**Target Issue:** #${item.item_number}\n\n`; + } else { + content += `**Target:** Current issue/PR\n\n`; + } + if (item.labels && item.labels.length > 0) { + content += `**Labels to add:** ${item.labels.join(", ")}\n\n`; + } + return content; + }, + }); return; } const allowedLabelsEnv = process.env.GH_AW_LABELS_ALLOWED?.trim(); @@ -1119,7 +1177,6 @@ jobs: group: "gh-aw-copilot-${{ github.workflow }}" env: GH_AW_SAFE_OUTPUTS: /tmp/gh-aw/safeoutputs/outputs.jsonl - GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1},\"add_labels\":{\"max\":5},\"missing_tool\":{}}" outputs: output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} @@ -1180,24 +1237,29 @@ jobs: main().catch(error => { core.setFailed(error instanceof Error ? error.message : String(error)); }); - - name: Validate COPILOT_CLI_TOKEN secret + - name: Validate COPILOT_GITHUB_TOKEN or COPILOT_CLI_TOKEN secret run: | - if [ -z "$COPILOT_CLI_TOKEN" ]; then - echo "Error: COPILOT_CLI_TOKEN secret is not set" - echo "The GitHub Copilot CLI engine requires the COPILOT_CLI_TOKEN secret to be configured." - echo "Please configure this secret in your repository settings." + if [ -z "$COPILOT_GITHUB_TOKEN" ] && [ -z "$COPILOT_CLI_TOKEN" ]; then + echo "Error: Neither COPILOT_GITHUB_TOKEN nor COPILOT_CLI_TOKEN secret is set" + echo "The GitHub Copilot CLI engine requires either COPILOT_GITHUB_TOKEN or COPILOT_CLI_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 - echo "COPILOT_CLI_TOKEN secret is configured" + if [ -n "$COPILOT_GITHUB_TOKEN" ]; then + echo "COPILOT_GITHUB_TOKEN secret is configured" + else + echo "COPILOT_CLI_TOKEN secret is configured (using as fallback for COPILOT_GITHUB_TOKEN)" + fi env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} COPILOT_CLI_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }} - name: Setup Node.js uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 with: node-version: '24' - name: Install GitHub Copilot CLI - run: npm install -g @github/copilot@0.0.353 + run: npm install -g @github/copilot@0.0.354 - name: Downloading container images run: | set -e @@ -1207,7 +1269,7 @@ jobs: run: | mkdir -p /tmp/gh-aw/safeoutputs cat > /tmp/gh-aw/safeoutputs/config.json << 'EOF' - {"add_comment":{"max":1},"add_labels":{"max":5},"missing_tool":{}} + {"add_comment":{"max":10,"target":"*"},"add_labels":{"max":100},"missing_tool":{}} EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -1231,39 +1293,26 @@ jobs: normalized = normalized.toLowerCase(); return normalized; } - const configEnv = process.env.GH_AW_SAFE_OUTPUTS_CONFIG; + const configPath = process.env.GH_AW_SAFE_OUTPUTS_CONFIG_PATH || "/tmp/gh-aw/safeoutputs/config.json"; let safeOutputsConfigRaw; - if (!configEnv) { - const defaultConfigPath = "/tmp/gh-aw/safeoutputs/config.json"; - debug(`GH_AW_SAFE_OUTPUTS_CONFIG not set, attempting to read from default path: ${defaultConfigPath}`); - try { - if (fs.existsSync(defaultConfigPath)) { - debug(`Reading config from file: ${defaultConfigPath}`); - const configFileContent = fs.readFileSync(defaultConfigPath, "utf8"); - debug(`Config file content length: ${configFileContent.length} characters`); - debug(`Config file read successfully, attempting to parse JSON`); - safeOutputsConfigRaw = JSON.parse(configFileContent); - debug(`Successfully parsed config from file with ${Object.keys(safeOutputsConfigRaw).length} configuration keys`); - } else { - debug(`Config file does not exist at: ${defaultConfigPath}`); - debug(`Using minimal default configuration`); - safeOutputsConfigRaw = {}; - } - } catch (error) { - debug(`Error reading config file: ${error instanceof Error ? error.message : String(error)}`); - debug(`Falling back to empty configuration`); + debug(`Reading config from file: ${configPath}`); + try { + if (fs.existsSync(configPath)) { + debug(`Config file exists at: ${configPath}`); + const configFileContent = fs.readFileSync(configPath, "utf8"); + debug(`Config file content length: ${configFileContent.length} characters`); + debug(`Config file read successfully, attempting to parse JSON`); + safeOutputsConfigRaw = JSON.parse(configFileContent); + debug(`Successfully parsed config from file with ${Object.keys(safeOutputsConfigRaw).length} configuration keys`); + } else { + debug(`Config file does not exist at: ${configPath}`); + debug(`Using minimal default configuration`); safeOutputsConfigRaw = {}; } - } else { - debug(`Using GH_AW_SAFE_OUTPUTS_CONFIG from environment variable`); - debug(`Config environment variable length: ${configEnv.length} characters`); - try { - safeOutputsConfigRaw = JSON.parse(configEnv); - debug(`Successfully parsed config from environment: ${JSON.stringify(safeOutputsConfigRaw)}`); - } catch (error) { - debug(`Error parsing config from environment: ${error instanceof Error ? error.message : String(error)}`); - throw new Error(`Failed to parse GH_AW_SAFE_OUTPUTS_CONFIG: ${error instanceof Error ? error.message : String(error)}`); - } + } catch (error) { + debug(`Error reading config file: ${error instanceof Error ? error.message : String(error)}`); + debug(`Falling back to empty configuration`); + safeOutputsConfigRaw = {}; } const safeOutputsConfig = Object.fromEntries(Object.entries(safeOutputsConfigRaw).map(([k, v]) => [k.replace(/-/g, "_"), v])); debug(`Final processed config: ${JSON.stringify(safeOutputsConfig)}`); @@ -1509,6 +1558,17 @@ jobs: }; }; function getCurrentBranch() { + const cwd = process.env.GITHUB_WORKSPACE || process.cwd(); + try { + const branch = execSync("git rev-parse --abbrev-ref HEAD", { + encoding: "utf8", + cwd: cwd, + }).trim(); + debug(`Resolved current branch from git in ${cwd}: ${branch}`); + return branch; + } catch (error) { + debug(`Failed to get branch from git: ${error instanceof Error ? error.message : String(error)}`); + } const ghHeadRef = process.env.GITHUB_HEAD_REF; const ghRefName = process.env.GITHUB_REF_NAME; if (ghHeadRef) { @@ -1519,23 +1579,22 @@ jobs: debug(`Resolved current branch from GITHUB_REF_NAME: ${ghRefName}`); return ghRefName; } - const cwd = process.env.GITHUB_WORKSPACE || process.cwd(); - try { - const branch = execSync("git rev-parse --abbrev-ref HEAD", { - encoding: "utf8", - cwd: cwd, - }).trim(); - debug(`Resolved current branch from git in ${cwd}: ${branch}`); - return branch; - } catch (error) { - throw new Error(`Failed to get current branch: ${error instanceof Error ? error.message : String(error)}`); - } + throw new Error("Failed to determine current branch: git command failed and no GitHub environment variables available"); + } + function getBaseBranch() { + return process.env.GH_AW_BASE_BRANCH || "main"; } const createPullRequestHandler = args => { const entry = { ...args, type: "create_pull_request" }; - if (!entry.branch || entry.branch.trim() === "") { - entry.branch = getCurrentBranch(); - debug(`Using current branch for create_pull_request: ${entry.branch}`); + const baseBranch = getBaseBranch(); + if (!entry.branch || entry.branch.trim() === "" || entry.branch === baseBranch) { + const detectedBranch = getCurrentBranch(); + if (entry.branch === baseBranch) { + debug(`Branch equals base branch (${baseBranch}), detecting actual working branch: ${detectedBranch}`); + } else { + debug(`Using current branch for create_pull_request: ${detectedBranch}`); + } + entry.branch = detectedBranch; } appendSafeOutput(entry); return { @@ -1549,9 +1608,15 @@ jobs: }; const pushToPullRequestBranchHandler = args => { const entry = { ...args, type: "push_to_pull_request_branch" }; - if (!entry.branch || entry.branch.trim() === "") { - entry.branch = getCurrentBranch(); - debug(`Using current branch for push_to_pull_request_branch: ${entry.branch}`); + const baseBranch = getBaseBranch(); + if (!entry.branch || entry.branch.trim() === "" || entry.branch === baseBranch) { + const detectedBranch = getCurrentBranch(); + if (entry.branch === baseBranch) { + debug(`Branch equals base branch (${baseBranch}), detecting actual working branch: ${detectedBranch}`); + } else { + debug(`Using current branch for push_to_pull_request_branch: ${detectedBranch}`); + } + entry.branch = detectedBranch; } appendSafeOutput(entry); return { @@ -1995,7 +2060,6 @@ jobs: env: GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_CONFIG: ${{ toJSON(env.GH_AW_SAFE_OUTPUTS_CONFIG) }} GH_AW_ASSETS_BRANCH: ${{ env.GH_AW_ASSETS_BRANCH }} GH_AW_ASSETS_MAX_SIZE_KB: ${{ env.GH_AW_ASSETS_MAX_SIZE_KB }} GH_AW_ASSETS_ALLOWED_EXTS: ${{ env.GH_AW_ASSETS_ALLOWED_EXTS }} @@ -2017,7 +2081,7 @@ jobs: "-e", "GITHUB_READ_ONLY=1", "-e", - "GITHUB_TOOLSETS=default", + "GITHUB_TOOLSETS=default,labels", "ghcr.io/github/github-mcp-server:v0.20.1" ], "tools": ["*"], @@ -2032,7 +2096,6 @@ jobs: "tools": ["*"], "env": { "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", - "GH_AW_SAFE_OUTPUTS_CONFIG": "\${GH_AW_SAFE_OUTPUTS_CONFIG}", "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", @@ -2065,29 +2128,39 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} run: | - mkdir -p $(dirname "$GH_AW_PROMPT") + PROMPT_DIR="$(dirname "$GH_AW_PROMPT")" + mkdir -p "$PROMPT_DIR" cat > "$GH_AW_PROMPT" << 'PROMPT_EOF' # Agentic Triage - You're a triage assistant for GitHub issues. Your task is to analyze issues created in the last 24 hours and perform initial triage tasks for each of them. + You're a triage assistant for GitHub issues. Your task is to analyze issues that were either created in the last 24 hours or updated (with a new comment) in the last 24 hours, and perform initial triage tasks for each of them. - 1. First, use the `list_issues` tool to retrieve all issues created in the last 24 hours. Filter issues by using the `since` parameter with a timestamp from 24 hours ago (calculate: current time minus 24 hours in ISO 8601 format). + 1. First, use the `list_issues` tool to retrieve all issues created or updated in the last 24 hours. The `since` parameter filters by the issue's `updated_at` timestamp, which includes both newly created issues and recently commented issues. Calculate the timestamp from 24 hours ago (example: 2025-11-06T20:27:14Z for reference) and use it for the `since` parameter. 2. For each issue found, perform the following triage tasks: - 3. Select appropriate labels for the issue from the provided list. + 3. Use the `get_comments` tool to retrieve all the comments on the issue. - 4. Retrieve the issue content using the `get_issue` tool. If the issue is obviously spam, or generated by bot, or something else that is not an actual issue to be worked on, then add an issue comment to the issue with a one sentence analysis and move to the next issue. + 4. Check for spam and quality issue descriptions and comments first: + + - **Non-English Content**: If the issue is primarily written in a non-English language, add a respectful and appreciative comment explaining that while you appreciate their contribution, the majority of the community communicates in English and kindly ask them to repost in English so everyone can follow along and help. Provide a friendly translation of your message in their language if possible. + - **Multiple Topics**: If the issue discusses multiple unrelated topics or problems, add a comment explaining that each issue should focus on one clear topic so the team can effectively solve the right problem. Politely ask them to split it into separate issues. + - **Obvious Spam or Bot-Generated Content**: If the issue/comment is obviously spam, generated by a bot, or something that is not an actual issue to be worked on, add an issue comment with a one-sentence analysis and move to the next issue. - 5. Next, use the GitHub tools to gather additional context about the issue: + 5. Retrieve the issue content using the `get_issue` tool for any issues that pass the spam checks. - - Fetch the list of labels available in this repository. Use 'gh label list' bash command to fetch the labels. This will give you the labels you can use for triaging issues. - - Fetch any comments on the issue using the `get_issue_comments` tool - - **Search for duplicate and related issues**: Use the `search_issues` tool to find similar issues by searching for key terms from the issue title and description. Look for both open and closed issues that might be related or duplicates. + 6. Next, use the GitHub tools to gather additional context about the issue: - 6. Analyze the issue content, considering: + - Fetch the list of labels available in this repository using the `list_label` tool with `owner: "appwrite"` and `repo: "appwrite"` parameters. This will give you the labels you can use for triaging issues. + - Fetch any comments on the issue using the `get_issue_comments` tool to understand recent activity + - **Search for duplicate and related issues (repo first, then org-wide)**: + - First search in this repository using the `search_issues` tool with a query like: `repo:appwrite/appwrite is:issue (is:open OR is:closed) `. + - Then perform an org-wide search across the entire Appwrite organization using: `org:appwrite is:issue (is:open OR is:closed) `. + - Prefer linking to OPEN issues when identifying potential duplicates; include CLOSED ones as related history when useful. + + 7. Analyze the issue content, considering: - The issue title and description - The type of issue (bug report, feature request, question, etc.) @@ -2096,45 +2169,48 @@ jobs: - User impact - Components affected - 7. Write notes, ideas, nudges, resource links, debugging strategies and/or reproduction steps for the team to consider relevant to the issue. + 8. Write notes, ideas, nudges, resource links, debugging strategies and/or reproduction steps for the team to consider relevant to the issue. - 8. Select appropriate labels from the available labels list provided above: + 9. Select appropriate labels from the available labels list: - Choose labels that accurately reflect the issue's nature - Be specific but comprehensive - Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority) - Consider platform labels (android, ios) if applicable - - Search for similar issues, and if you find similar issues consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue. - - Only select labels from the provided list above + - Search for similar issues. If you find a duplicate of another OPEN issue in THIS repository, you may use a "duplicate" label (if available) and reference the canonical issue. + - If the closest match is in another repository within the Appwrite org, do NOT mark as duplicate here; instead, link it in your comment under a "Cross‑repo related issues" section. + - Only select labels from the provided list + - Don't apply the `good first issue` or `help wanted` labels - It's okay to not add any labels if none are clearly applicable - 9. Apply the selected labels: + 10. Apply the selected labels: - Use the `update_issue` tool to apply the labels to the issue - DO NOT communicate directly with users - If no labels are clearly applicable, do not apply any labels - 10. Add an issue comment to the issue with your analysis: + 11. Add an issue comment to the issue with your analysis: - Start with "🎯 Agentic Issue Triage" - Provide a brief summary of the issue - - **If duplicate or related issues were found**, add a section listing them with links (e.g., "### πŸ”— Potentially Related Issues" followed by a bullet list of related issues with their titles and links) + - **If duplicate or related issues were found**, add sections listing them with links: + - "### πŸ”— Potentially Related Issues (this repo)" – bullet list of same-repo issues with titles and links + - If applicable: "### 🌐 Cross-repo related issues (org: appwrite)" – bullet list including `owner/repo#number` with titles and links - Mention any relevant details that might help the team understand the issue better - Include any debugging strategies or reproduction steps if applicable - Suggest resources or links that might be helpful for resolving the issue or learning skills related to the issue or the particular area of the codebase affected by it - Mention any nudges or ideas that could help the team in addressing the issue - - If you have possible reproduction steps, include them in the comment - - If you have any debugging strategies, include them in the comment - - If appropriate break the issue down to sub-tasks and write a checklist of things to do. - - Use collapsed-by-default sections in the GitHub markdown to keep the comment tidy. Collapse all sections except the short main summary at the top. + - If appropriate break the issue down to sub-tasks and write a checklist of things to do + - Use collapsed-by-default sections in the GitHub markdown to keep the comment tidy. Collapse all sections except the short main summary at the top. For bolded section titles, wrap the text with `` and `` to make it bold. + - Do not indicate/encourage a community member to submit a PR for the issue. - 11. After processing all issues, provide a summary of how many issues were triaged. If no issues were created in the last 24 hours, simply note that no new issues needed triage. + 12. After processing all issues, provide a summary of how many issues were triaged (created or updated in the last 24 hours). If no issues matched the criteria, simply note that no issues needed triage. PROMPT_EOF - name: Append XPIA security instructions to prompt env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt run: | - cat >> "$GH_AW_PROMPT" << 'PROMPT_EOF' + cat >> "$GH_AW_PROMPT" << PROMPT_EOF --- @@ -2166,7 +2242,7 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt run: | - cat >> "$GH_AW_PROMPT" << 'PROMPT_EOF' + cat >> "$GH_AW_PROMPT" << PROMPT_EOF --- @@ -2179,7 +2255,7 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt run: | - cat >> "$GH_AW_PROMPT" << 'PROMPT_EOF' + cat >> "$GH_AW_PROMPT" << PROMPT_EOF --- @@ -2204,7 +2280,7 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt run: | - cat >> "$GH_AW_PROMPT" << 'PROMPT_EOF' + cat >> "$GH_AW_PROMPT" << PROMPT_EOF --- @@ -2234,7 +2310,7 @@ jobs: Use this context information to understand the scope of your work. PROMPT_EOF - - name: Render template conditionals + - name: Interpolate variables and render templates uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -2245,45 +2321,74 @@ jobs: const v = expr.trim().toLowerCase(); return !(v === "" || v === "false" || v === "0" || v === "null" || v === "undefined"); } + function interpolateVariables(content, variables) { + let result = content; + for (const [varName, value] of Object.entries(variables)) { + const pattern = new RegExp(`\\$\\{${varName}\\}`, "g"); + result = result.replace(pattern, value); + } + return result; + } function renderMarkdownTemplate(markdown) { return markdown.replace(/{{#if\s+([^}]+)}}([\s\S]*?){{\/if}}/g, (_, cond, body) => (isTruthy(cond) ? body : "")); } - function main() { + async function main() { try { const promptPath = process.env.GH_AW_PROMPT; if (!promptPath) { core.setFailed("GH_AW_PROMPT environment variable is not set"); - process.exit(1); + return; } - const markdown = fs.readFileSync(promptPath, "utf8"); - const hasConditionals = /{{#if\s+[^}]+}}/.test(markdown); - if (!hasConditionals) { + let content = fs.readFileSync(promptPath, "utf8"); + const variables = {}; + for (const [key, value] of Object.entries(process.env)) { + if (key.startsWith("GH_AW_EXPR_")) { + variables[key] = value || ""; + } + } + const varCount = Object.keys(variables).length; + if (varCount > 0) { + core.info(`Found ${varCount} expression variable(s) to interpolate`); + content = interpolateVariables(content, variables); + core.info(`Successfully interpolated ${varCount} variable(s) in prompt`); + } else { + core.info("No expression variables found, skipping interpolation"); + } + const hasConditionals = /{{#if\s+[^}]+}}/.test(content); + if (hasConditionals) { + core.info("Processing conditional template blocks"); + content = renderMarkdownTemplate(content); + core.info("Template rendered successfully"); + } else { core.info("No conditional blocks found in prompt, skipping template rendering"); - process.exit(0); } - const rendered = renderMarkdownTemplate(markdown); - fs.writeFileSync(promptPath, rendered, "utf8"); - core.info("Template rendered successfully"); + fs.writeFileSync(promptPath, content, "utf8"); } catch (error) { core.setFailed(error instanceof Error ? error.message : String(error)); } } main(); - - name: Print prompt to step summary + - name: Print prompt env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt run: | - echo "
" >> "$GITHUB_STEP_SUMMARY" - echo "Generated Prompt" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo '```markdown' >> "$GITHUB_STEP_SUMMARY" - cat "$GH_AW_PROMPT" >> "$GITHUB_STEP_SUMMARY" - echo '```' >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "
" >> "$GITHUB_STEP_SUMMARY" + # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 with: name: prompt.txt path: /tmp/gh-aw/aw-prompts/prompt.txt @@ -2299,7 +2404,7 @@ jobs: engine_name: "GitHub Copilot CLI", model: "", version: "", - agent_version: "0.0.353", + agent_version: "0.0.354", workflow_name: "Agentic Triage", experimental: false, supports_tools_allowlist: true, @@ -2326,7 +2431,7 @@ jobs: console.log(JSON.stringify(awInfo, null, 2)); - name: Upload agentic run info if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 with: name: aw_info.json path: /tmp/gh-aw/aw_info.json @@ -2340,7 +2445,7 @@ jobs: timeout-minutes: 10 run: | set -o pipefail - COPILOT_CLI_INSTRUCTION=$(cat /tmp/gh-aw/aw-prompts/prompt.txt) + 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/ @@ -2348,15 +2453,14 @@ jobs: copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/.copilot/logs/ --disable-builtin-mcps --allow-tool github --allow-tool safeoutputs --allow-tool web-fetch --prompt "$COPILOT_CLI_INSTRUCTION" 2>&1 | tee /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN || secrets.COPILOT_CLI_TOKEN }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1},\"add_labels\":{\"max\":5},\"missing_tool\":{}}" GITHUB_HEAD_REF: ${{ github.head_ref }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} - GITHUB_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }} GITHUB_WORKSPACE: ${{ github.workspace }} XDG_CONFIG_HOME: /home/runner - name: Redact secrets in logs @@ -2470,13 +2574,14 @@ jobs: } await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_CLI_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + GH_AW_SECRET_NAMES: 'COPILOT_CLI_TOKEN,COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_COPILOT_CLI_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }} + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Safe Outputs if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 with: name: safe_output.jsonl path: ${{ env.GH_AW_SAFE_OUTPUTS }} @@ -2486,24 +2591,58 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd env: GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1},\"add_labels\":{\"max\":5},\"missing_tool\":{}}" GH_AW_ALLOWED_DOMAINS: "api.enterprise.githubcopilot.com,api.github.com,github.com,raw.githubusercontent.com,registry.npmjs.org" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} with: script: | async function main() { const fs = require("fs"); + function extractDomainsFromUrl(url) { + if (!url || typeof url !== "string") { + return []; + } + try { + const urlObj = new URL(url); + const hostname = urlObj.hostname.toLowerCase(); + const domains = [hostname]; + if (hostname === "github.com") { + domains.push("api.github.com"); + domains.push("raw.githubusercontent.com"); + domains.push("*.githubusercontent.com"); + } + else if (!hostname.startsWith("api.")) { + domains.push("api." + hostname); + domains.push("raw." + hostname); + } + return domains; + } catch (e) { + return []; + } + } function sanitizeContent(content, maxLength) { if (!content || typeof content !== "string") { return ""; } const allowedDomainsEnv = process.env.GH_AW_ALLOWED_DOMAINS; const defaultAllowedDomains = ["github.com", "github.io", "githubusercontent.com", "githubassets.com", "github.dev", "codespaces.new"]; - const allowedDomains = allowedDomainsEnv + let allowedDomains = allowedDomainsEnv ? allowedDomainsEnv .split(",") .map(d => d.trim()) .filter(d => d) : defaultAllowedDomains; + const githubServerUrl = process.env.GITHUB_SERVER_URL; + const githubApiUrl = process.env.GITHUB_API_URL; + if (githubServerUrl) { + const serverDomains = extractDomainsFromUrl(githubServerUrl); + allowedDomains = allowedDomains.concat(serverDomains); + } + if (githubApiUrl) { + const apiDomains = extractDomainsFromUrl(githubApiUrl); + allowedDomains = allowedDomains.concat(apiDomains); + } + allowedDomains = [...new Set(allowedDomains)]; let sanitized = content; sanitized = neutralizeCommands(sanitized); sanitized = neutralizeMentions(sanitized); @@ -2918,7 +3057,16 @@ jobs: } } const outputFile = process.env.GH_AW_SAFE_OUTPUTS; - const safeOutputsConfig = process.env.GH_AW_SAFE_OUTPUTS_CONFIG; + const configPath = process.env.GH_AW_SAFE_OUTPUTS_CONFIG_PATH || "/tmp/gh-aw/safeoutputs/config.json"; + let safeOutputsConfig; + try { + if (fs.existsSync(configPath)) { + const configFileContent = fs.readFileSync(configPath, "utf8"); + safeOutputsConfig = JSON.parse(configFileContent); + } + } catch (error) { + core.warning(`Failed to read config file from ${configPath}: ${error instanceof Error ? error.message : String(error)}`); + } if (!outputFile) { core.info("GH_AW_SAFE_OUTPUTS not set, no output to collect"); core.setOutput("output", ""); @@ -2937,8 +3085,7 @@ jobs: let expectedOutputTypes = {}; if (safeOutputsConfig) { try { - const rawConfig = JSON.parse(safeOutputsConfig); - expectedOutputTypes = Object.fromEntries(Object.entries(rawConfig).map(([key, value]) => [key.replace(/-/g, "_"), value])); + expectedOutputTypes = Object.fromEntries(Object.entries(safeOutputsConfig).map(([key, value]) => [key.replace(/-/g, "_"), value])); core.info(`Expected output types: ${JSON.stringify(Object.keys(expectedOutputTypes))}`); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); @@ -3309,13 +3456,13 @@ jobs: await main(); - name: Upload sanitized agent output if: always() && env.GH_AW_AGENT_OUTPUT - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 with: name: agent_output.json path: ${{ env.GH_AW_AGENT_OUTPUT }} if-no-files-found: warn - name: Upload engine output files - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 with: name: agent_outputs path: | @@ -3323,7 +3470,7 @@ jobs: if-no-files-found: ignore - name: Upload MCP logs if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 with: name: mcp-logs path: /tmp/gh-aw/mcp-logs/ @@ -3335,9 +3482,10 @@ jobs: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/.copilot/logs/ with: script: | - function main() { + 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) { @@ -3351,6 +3499,10 @@ jobs: 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) { @@ -3361,26 +3513,55 @@ jobs: for (const file of logFiles) { const filePath = path.join(logPath, file); const fileContent = fs.readFileSync(filePath, "utf8"); - content += fileContent; if (content.length > 0 && !content.endsWith("\n")) { content += "\n"; } + content += fileContent; } } else { content = fs.readFileSync(logPath, "utf8"); } - const parsedLog = parseCopilotLog(content); - if (parsedLog) { - core.info(parsedLog); - core.summary.addRaw(parsedLog).write(); - core.info("Copilot log parsed successfully"); + const result = parseLog(content); + let markdown = ""; + let mcpFailures = []; + let maxTurnsHit = false; + if (typeof result === "string") { + markdown = result; + } else if (result && typeof result === "object") { + markdown = result.markdown || ""; + mcpFailures = result.mcpFailures || []; + maxTurnsHit = result.maxTurnsHit || false; + } + if (markdown) { + core.info(markdown); + core.summary.addRaw(markdown).write(); + core.info(`${parserName} log parsed successfully`); } else { - core.error("Failed to parse Copilot log"); + 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)); } } + if (typeof module !== "undefined" && module.exports) { + module.exports = { + runLogParser, + }; + } + function main() { + runLogParser({ + parseLog: parseCopilotLog, + parserName: "Copilot", + supportsDirectories: true, + }); + } function extractPremiumRequestCount(logContent) { const patterns = [ /premium\s+requests?\s+consumed:?\s*(\d+)/i, @@ -4208,7 +4389,7 @@ jobs: main(); - name: Upload Agent Stdio if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 with: name: agent-stdio.log path: /tmp/gh-aw/agent-stdio.log @@ -4590,24 +4771,29 @@ jobs: run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - - name: Validate COPILOT_CLI_TOKEN secret + - name: Validate COPILOT_GITHUB_TOKEN or COPILOT_CLI_TOKEN secret run: | - if [ -z "$COPILOT_CLI_TOKEN" ]; then - echo "Error: COPILOT_CLI_TOKEN secret is not set" - echo "The GitHub Copilot CLI engine requires the COPILOT_CLI_TOKEN secret to be configured." - echo "Please configure this secret in your repository settings." + if [ -z "$COPILOT_GITHUB_TOKEN" ] && [ -z "$COPILOT_CLI_TOKEN" ]; then + echo "Error: Neither COPILOT_GITHUB_TOKEN nor COPILOT_CLI_TOKEN secret is set" + echo "The GitHub Copilot CLI engine requires either COPILOT_GITHUB_TOKEN or COPILOT_CLI_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 - echo "COPILOT_CLI_TOKEN secret is configured" + if [ -n "$COPILOT_GITHUB_TOKEN" ]; then + echo "COPILOT_GITHUB_TOKEN secret is configured" + else + echo "COPILOT_CLI_TOKEN secret is configured (using as fallback for COPILOT_GITHUB_TOKEN)" + fi env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} COPILOT_CLI_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }} - name: Setup Node.js uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 with: node-version: '24' - name: Install GitHub Copilot CLI - run: npm install -g @github/copilot@0.0.353 + run: npm install -g @github/copilot@0.0.354 - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): @@ -4621,7 +4807,7 @@ jobs: timeout-minutes: 20 run: | set -o pipefail - COPILOT_CLI_INSTRUCTION=$(cat /tmp/gh-aw/aw-prompts/prompt.txt) + 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/ @@ -4629,11 +4815,11 @@ jobs: copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/.copilot/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --prompt "$COPILOT_CLI_INSTRUCTION" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN || secrets.COPILOT_CLI_TOKEN }} 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_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }} GITHUB_WORKSPACE: ${{ github.workspace }} XDG_CONFIG_HOME: /home/runner - name: Parse threat detection results @@ -4674,7 +4860,7 @@ jobs: } - name: Upload threat detection log if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 with: name: threat-detection.log path: /tmp/gh-aw/threat-detection/detection.log @@ -4702,8 +4888,8 @@ jobs: - name: Setup agent output environment variable run: | mkdir -p /tmp/gh-aw/safeoutputs/ - find /tmp/gh-aw/safeoutputs/ -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> $GITHUB_ENV + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - name: Record Missing Tool id: missing_tool uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd @@ -4821,7 +5007,7 @@ jobs: id: check_stop_time uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd env: - GH_AW_STOP_TIME: 2025-12-03 20:01:19 + GH_AW_STOP_TIME: 2025-12-12 19:43:36 GH_AW_WORKFLOW_NAME: "Agentic Triage" with: script: | @@ -4891,8 +5077,8 @@ jobs: - name: Setup agent output environment variable run: | mkdir -p /tmp/gh-aw/safeoutputs/ - find /tmp/gh-aw/safeoutputs/ -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> $GITHUB_ENV + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - name: Update reaction comment with completion status id: update_reaction uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd diff --git a/.github/workflows/issue-triage.md b/.github/workflows/issue-triage.md index 087f009106..8b327a26cf 100644 --- a/.github/workflows/issue-triage.md +++ b/.github/workflows/issue-triage.md @@ -8,41 +8,68 @@ on: permissions: read-all +# Add stricter error-detection patterns so issue text doesn't trigger agent error detection. +# After merging this file run `gh aw compile` to regenerate the lock file. +env: + GH_AW_ERROR_PATTERNS: >- + [ + {"id":"gh-action-error","pattern":"^::(error)(?:\\\\s+[^:]*)?::(.+)","level_group":1,"message_group":2,"description":"GitHub Actions workflow command - error"}, + {"id":"gh-action-warning","pattern":"^::(warning)(?:\\\\s+[^:]*)?::(.+)","level_group":1,"message_group":2,"description":"GitHub Actions workflow command - warning"}, + {"id":"bracketed-level","pattern":"^\\[(ERROR|CRITICAL|WARNING|WARN)\\]\\s+(.+)","level_group":1,"message_group":2,"description":"Bracketed log level at start of line"}, + {"id":"timestamped-copilot","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z\\s+\\[(ERROR|WARN|WARNING|CRITICAL)\\]\\s+(.+)","level_group":1,"message_group":3,"description":"Timestamped Copilot CLI messages"} + ] + network: defaults safe-outputs: add-labels: - max: 5 + max: 100 + target: "*" add-comment: + max: 10 + target: "*" tools: web-fetch: web-search: + github: + toolsets: + - default + - labels -timeout_minutes: 10 +timeout-minutes: 10 source: githubnext/agentics/workflows/issue-triage.md@0837fb7b24c3b84ee77fb7c8cfa8735c48be347a --- # Agentic Triage -You're a triage assistant for GitHub issues. Your task is to analyze issues created in the last 24 hours and perform initial triage tasks for each of them. +You're a triage assistant for GitHub issues. Your task is to analyze issues that were either created in the last 24 hours or updated (with a new comment) in the last 24 hours, and perform initial triage tasks for each of them. -1. First, use the `list_issues` tool to retrieve all issues created in the last 24 hours. Filter issues by using the `since` parameter with a timestamp from 24 hours ago (calculate: current time minus 24 hours in ISO 8601 format). +1. First, use the `list_issues` tool to retrieve all issues created or updated in the last 24 hours. The `since` parameter filters by the issue's `updated_at` timestamp, which includes both newly created issues and recently commented issues. Calculate the timestamp from 24 hours ago (example: 2025-11-06T20:27:14Z for reference) and use it for the `since` parameter. 2. For each issue found, perform the following triage tasks: -3. Select appropriate labels for the issue from the provided list. +3. Use the `get_comments` tool to retrieve all the comments on the issue. -4. Retrieve the issue content using the `get_issue` tool. If the issue is obviously spam, or generated by bot, or something else that is not an actual issue to be worked on, then add an issue comment to the issue with a one sentence analysis and move to the next issue. +4. Check for spam and quality issue descriptions and comments first: + + - **Non-English Content**: If the issue is primarily written in a non-English language, add a respectful and appreciative comment explaining that while you appreciate their contribution, the majority of the community communicates in English and kindly ask them to repost in English so everyone can follow along and help. Provide a friendly translation of your message in their language if possible. + - **Multiple Topics**: If the issue discusses multiple unrelated topics or problems, add a comment explaining that each issue should focus on one clear topic so the team can effectively solve the right problem. Politely ask them to split it into separate issues. + - **Obvious Spam or Bot-Generated Content**: If the issue/comment is obviously spam, generated by a bot, or something that is not an actual issue to be worked on, add an issue comment with a one-sentence analysis and move to the next issue. -5. Next, use the GitHub tools to gather additional context about the issue: +5. Retrieve the issue content using the `get_issue` tool for any issues that pass the spam checks. - - Fetch the list of labels available in this repository. Use 'gh label list' bash command to fetch the labels. This will give you the labels you can use for triaging issues. - - Fetch any comments on the issue using the `get_issue_comments` tool - - **Search for duplicate and related issues**: Use the `search_issues` tool to find similar issues by searching for key terms from the issue title and description. Look for both open and closed issues that might be related or duplicates. +6. Next, use the GitHub tools to gather additional context about the issue: -6. Analyze the issue content, considering: + - Fetch the list of labels available in this repository using the `list_label` tool with `owner: "appwrite"` and `repo: "appwrite"` parameters. This will give you the labels you can use for triaging issues. + - Fetch any comments on the issue using the `get_issue_comments` tool to understand recent activity + - **Search for duplicate and related issues (repo first, then org-wide)**: + - First search in this repository using the `search_issues` tool with a query like: `repo:appwrite/appwrite is:issue (is:open OR is:closed) `. + - Then perform an org-wide search across the entire Appwrite organization using: `org:appwrite is:issue (is:open OR is:closed) `. + - Prefer linking to OPEN issues when identifying potential duplicates; include CLOSED ones as related history when useful. + +7. Analyze the issue content, considering: - The issue title and description - The type of issue (bug report, feature request, question, etc.) @@ -51,35 +78,38 @@ You're a triage assistant for GitHub issues. Your task is to analyze issues crea - User impact - Components affected -7. Write notes, ideas, nudges, resource links, debugging strategies and/or reproduction steps for the team to consider relevant to the issue. +8. Write notes, ideas, nudges, resource links, debugging strategies and/or reproduction steps for the team to consider relevant to the issue. -8. Select appropriate labels from the available labels list provided above: +9. Select appropriate labels from the available labels list: - Choose labels that accurately reflect the issue's nature - Be specific but comprehensive - Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority) - Consider platform labels (android, ios) if applicable - - Search for similar issues, and if you find similar issues consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue. - - Only select labels from the provided list above + - Search for similar issues. If you find a duplicate of another OPEN issue in THIS repository, you may use a "duplicate" label (if available) and reference the canonical issue. + - If the closest match is in another repository within the Appwrite org, do NOT mark as duplicate here; instead, link it in your comment under a "Cross‑repo related issues" section. + - Only select labels from the provided list + - Don't apply the `good first issue` or `help wanted` labels - It's okay to not add any labels if none are clearly applicable -9. Apply the selected labels: +10. Apply the selected labels: - Use the `update_issue` tool to apply the labels to the issue - DO NOT communicate directly with users - If no labels are clearly applicable, do not apply any labels -10. Add an issue comment to the issue with your analysis: +11. Add an issue comment to the issue with your analysis: - Start with "🎯 Agentic Issue Triage" - Provide a brief summary of the issue - - **If duplicate or related issues were found**, add a section listing them with links (e.g., "### πŸ”— Potentially Related Issues" followed by a bullet list of related issues with their titles and links) + - **If duplicate or related issues were found**, add sections listing them with links: + - "### πŸ”— Potentially Related Issues (this repo)" – bullet list of same-repo issues with titles and links + - If applicable: "### 🌐 Cross-repo related issues (org: appwrite)" – bullet list including `owner/repo#number` with titles and links - Mention any relevant details that might help the team understand the issue better - Include any debugging strategies or reproduction steps if applicable - Suggest resources or links that might be helpful for resolving the issue or learning skills related to the issue or the particular area of the codebase affected by it - Mention any nudges or ideas that could help the team in addressing the issue - - If you have possible reproduction steps, include them in the comment - - If you have any debugging strategies, include them in the comment - - If appropriate break the issue down to sub-tasks and write a checklist of things to do. - - Use collapsed-by-default sections in the GitHub markdown to keep the comment tidy. Collapse all sections except the short main summary at the top. + - If appropriate break the issue down to sub-tasks and write a checklist of things to do + - Use collapsed-by-default sections in the GitHub markdown to keep the comment tidy. Collapse all sections except the short main summary at the top. For bolded section titles, wrap the text with `` and `` to make it bold. + - Do not indicate/encourage a community member to submit a PR for the issue. -11. After processing all issues, provide a summary of how many issues were triaged. If no issues were created in the last 24 hours, simply note that no new issues needed triage. +12. After processing all issues, provide a summary of how many issues were triaged (created or updated in the last 24 hours). If no issues matched the criteria, simply note that no issues needed triage.