diff --git a/.github/workflows/artifacts-summary.lock.yml b/.github/workflows/artifacts-summary.lock.yml index db4d3d4838..301da96173 100644 --- a/.github/workflows/artifacts-summary.lock.yml +++ b/.github/workflows/artifacts-summary.lock.yml @@ -494,7 +494,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -504,6 +504,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/audit-workflows.lock.yml b/.github/workflows/audit-workflows.lock.yml index ceb1c8a470..2a1009d6a2 100644 --- a/.github/workflows/audit-workflows.lock.yml +++ b/.github/workflows/audit-workflows.lock.yml @@ -638,7 +638,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -648,6 +648,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/brave.lock.yml b/.github/workflows/brave.lock.yml index 4ab8bcb471..4089c09ea8 100644 --- a/.github/workflows/brave.lock.yml +++ b/.github/workflows/brave.lock.yml @@ -959,7 +959,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -969,6 +969,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, @@ -3642,13 +3646,14 @@ jobs: - agent - detection if: > - ((always()) && (contains(needs.agent.outputs.output_types, 'add-comment'))) && ((github.event.issue.number) || - (github.event.pull_request.number)) + ((always()) && (contains(needs.agent.outputs.output_types, 'add-comment'))) && (github.event.issue.number || + github.event.pull_request.number || github.event.discussion.number) runs-on: ubuntu-latest permissions: contents: read issues: write pull-requests: write + discussions: write timeout-minutes: 10 outputs: comment_id: ${{ steps.add_comment.outputs.comment_id }} @@ -3745,8 +3750,9 @@ jobs: context.eventName === "pull_request" || context.eventName === "pull_request_review" || context.eventName === "pull_request_review_comment"; - if (commentTarget === "triggering" && !isIssueContext && !isPRContext) { - core.info('Target is "triggering" but not running in issue or pull request context, skipping comment creation'); + const isDiscussionContext = context.eventName === "discussion" || context.eventName === "discussion_comment"; + if (commentTarget === "triggering" && !isIssueContext && !isPRContext && !isDiscussionContext) { + core.info('Target is "triggering" but not running in issue, pull request, or discussion context, skipping comment creation'); return; } const createdComments = []; @@ -3754,6 +3760,7 @@ jobs: const commentItem = commentItems[i]; core.info(`Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`); let issueNumber; + let discussionId; let commentEndpoint; if (commentTarget === "*") { if (commentItem.issue_number) { @@ -3763,8 +3770,15 @@ jobs: continue; } commentEndpoint = "issues"; + } else if (commentItem.discussion_number) { + issueNumber = parseInt(commentItem.discussion_number, 10); + if (isNaN(issueNumber) || issueNumber <= 0) { + core.info(`Invalid discussion number specified: ${commentItem.discussion_number}`); + continue; + } + commentEndpoint = "discussions"; } else { - core.info('Target is "*" but no issue_number specified in comment item'); + core.info('Target is "*" but no issue_number or discussion_number specified in comment item'); continue; } } else if (commentTarget && commentTarget !== "triggering") { @@ -3791,10 +3805,19 @@ jobs: core.info("Pull request context detected but no pull request found in payload"); continue; } + } else if (isDiscussionContext) { + if (context.payload.discussion) { + issueNumber = context.payload.discussion.number; + discussionId = context.payload.discussion.node_id; + commentEndpoint = "discussions"; + } else { + core.info("Discussion context detected but no discussion found in payload"); + continue; + } } } if (!issueNumber) { - core.info("Could not determine issue or pull request number"); + core.info("Could not determine issue, pull request, or discussion number"); continue; } let body = commentItem.body.trim(); @@ -3810,13 +3833,59 @@ jobs: core.info(`Creating comment on ${commentEndpoint} #${issueNumber}`); core.info(`Comment content length: ${body.length}`); try { - const { data: comment } = await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body: body, - }); - core.info("Created comment #" + comment.id + ": " + comment.html_url); + let comment; + if (commentEndpoint === "discussions") { + if (!discussionId) { + const discussionQuery = ` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + discussion(number: $number) { + id + } + } + } + `; + const queryResult = await github.graphql(discussionQuery, { + owner: context.repo.owner, + repo: context.repo.repo, + number: issueNumber, + }); + discussionId = queryResult.repository.discussion.id; + } + const addDiscussionCommentMutation = ` + mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: { + discussionId: $discussionId, + body: $body + }) { + comment { + id + url + databaseId + } + } + } + `; + const mutationResult = await github.graphql(addDiscussionCommentMutation, { + discussionId: discussionId, + body: body, + }); + const discussionComment = mutationResult.addDiscussionComment.comment; + comment = { + id: discussionComment.databaseId, + html_url: discussionComment.url, + }; + core.info("Created discussion comment #" + comment.id + ": " + comment.html_url); + } else { + const { data: restComment } = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: body, + }); + comment = restComment; + core.info("Created comment #" + comment.id + ": " + comment.html_url); + } createdComments.push(comment); if (i === commentItems.length - 1) { core.setOutput("comment_id", comment.id); diff --git a/.github/workflows/changeset-generator.lock.yml b/.github/workflows/changeset-generator.lock.yml index b83d7d716f..73e3810f0d 100644 --- a/.github/workflows/changeset-generator.lock.yml +++ b/.github/workflows/changeset-generator.lock.yml @@ -929,7 +929,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -939,6 +939,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index cf52da4e47..c58d6438e0 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -465,7 +465,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -475,6 +475,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, @@ -3427,13 +3431,14 @@ jobs: - agent - detection if: > - ((always()) && (contains(needs.agent.outputs.output_types, 'add-comment'))) && ((github.event.issue.number) || - (github.event.pull_request.number)) + ((always()) && (contains(needs.agent.outputs.output_types, 'add-comment'))) && (github.event.issue.number || + github.event.pull_request.number || github.event.discussion.number) runs-on: ubuntu-latest permissions: contents: read issues: write pull-requests: write + discussions: write timeout-minutes: 10 outputs: comment_id: ${{ steps.add_comment.outputs.comment_id }} @@ -3532,8 +3537,9 @@ jobs: context.eventName === "pull_request" || context.eventName === "pull_request_review" || context.eventName === "pull_request_review_comment"; - if (commentTarget === "triggering" && !isIssueContext && !isPRContext) { - core.info('Target is "triggering" but not running in issue or pull request context, skipping comment creation'); + const isDiscussionContext = context.eventName === "discussion" || context.eventName === "discussion_comment"; + if (commentTarget === "triggering" && !isIssueContext && !isPRContext && !isDiscussionContext) { + core.info('Target is "triggering" but not running in issue, pull request, or discussion context, skipping comment creation'); return; } const createdComments = []; @@ -3541,6 +3547,7 @@ jobs: const commentItem = commentItems[i]; core.info(`Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`); let issueNumber; + let discussionId; let commentEndpoint; if (commentTarget === "*") { if (commentItem.issue_number) { @@ -3550,8 +3557,15 @@ jobs: continue; } commentEndpoint = "issues"; + } else if (commentItem.discussion_number) { + issueNumber = parseInt(commentItem.discussion_number, 10); + if (isNaN(issueNumber) || issueNumber <= 0) { + core.info(`Invalid discussion number specified: ${commentItem.discussion_number}`); + continue; + } + commentEndpoint = "discussions"; } else { - core.info('Target is "*" but no issue_number specified in comment item'); + core.info('Target is "*" but no issue_number or discussion_number specified in comment item'); continue; } } else if (commentTarget && commentTarget !== "triggering") { @@ -3578,10 +3592,19 @@ jobs: core.info("Pull request context detected but no pull request found in payload"); continue; } + } else if (isDiscussionContext) { + if (context.payload.discussion) { + issueNumber = context.payload.discussion.number; + discussionId = context.payload.discussion.node_id; + commentEndpoint = "discussions"; + } else { + core.info("Discussion context detected but no discussion found in payload"); + continue; + } } } if (!issueNumber) { - core.info("Could not determine issue or pull request number"); + core.info("Could not determine issue, pull request, or discussion number"); continue; } let body = commentItem.body.trim(); @@ -3597,13 +3620,59 @@ jobs: core.info(`Creating comment on ${commentEndpoint} #${issueNumber}`); core.info(`Comment content length: ${body.length}`); try { - const { data: comment } = await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body: body, - }); - core.info("Created comment #" + comment.id + ": " + comment.html_url); + let comment; + if (commentEndpoint === "discussions") { + if (!discussionId) { + const discussionQuery = ` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + discussion(number: $number) { + id + } + } + } + `; + const queryResult = await github.graphql(discussionQuery, { + owner: context.repo.owner, + repo: context.repo.repo, + number: issueNumber, + }); + discussionId = queryResult.repository.discussion.id; + } + const addDiscussionCommentMutation = ` + mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: { + discussionId: $discussionId, + body: $body + }) { + comment { + id + url + databaseId + } + } + } + `; + const mutationResult = await github.graphql(addDiscussionCommentMutation, { + discussionId: discussionId, + body: body, + }); + const discussionComment = mutationResult.addDiscussionComment.comment; + comment = { + id: discussionComment.databaseId, + html_url: discussionComment.url, + }; + core.info("Created discussion comment #" + comment.id + ": " + comment.html_url); + } else { + const { data: restComment } = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: body, + }); + comment = restComment; + core.info("Created comment #" + comment.id + ": " + comment.html_url); + } createdComments.push(comment); if (i === commentItems.length - 1) { core.setOutput("comment_id", comment.id); diff --git a/.github/workflows/cli-version-checker.lock.yml b/.github/workflows/cli-version-checker.lock.yml index 31ef7395e0..2e07ad4b6b 100644 --- a/.github/workflows/cli-version-checker.lock.yml +++ b/.github/workflows/cli-version-checker.lock.yml @@ -600,7 +600,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -610,6 +610,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/daily-news.lock.yml b/.github/workflows/daily-news.lock.yml index add94836e2..dad9e1544e 100644 --- a/.github/workflows/daily-news.lock.yml +++ b/.github/workflows/daily-news.lock.yml @@ -496,7 +496,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -506,6 +506,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index f123c87f13..273579a3f5 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -5,15 +5,23 @@ name: "Dev" on: - push: - paths: - - .github/workflows/dev.lock.yml - workflow_dispatch: null + discussion: + types: + - created + discussion_comment: + types: + - created + workflow_dispatch: + inputs: + discussion_number: + description: Discussion number to add comment to + required: true + type: string permissions: {} concurrency: - group: "gh-aw-${{ github.workflow }}-${{ github.ref }}" + group: "gh-aw-${{ github.workflow }}-${{ github.event.discussion.number }}" run-name: "Dev" @@ -134,9 +142,10 @@ jobs: permissions: actions: read contents: read + discussions: write env: GITHUB_AW_SAFE_OUTPUTS: /tmp/gh-aw/safe-outputs/outputs.jsonl - GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"create-issue\":{\"max\":1},\"missing-tool\":{}}" + GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"max\":1,\"target\":\"*\"},\"missing-tool\":{}}" outputs: output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} @@ -199,7 +208,7 @@ jobs: run: | mkdir -p /tmp/gh-aw/safe-outputs cat > /tmp/gh-aw/safe-outputs/config.json << 'EOF' - {"create-issue":{"max":1},"missing-tool":{}} + {"add-comment":{"max":1,"target":"*"},"missing-tool":{}} EOF cat > /tmp/gh-aw/safe-outputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -493,7 +502,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -503,6 +512,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, @@ -873,7 +886,7 @@ jobs: - name: Setup MCPs env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"create-issue\":{\"max\":1},\"missing-tool\":{}}" + GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"max\":1,\"target\":\"*\"},\"missing-tool\":{}}" run: | mkdir -p /tmp/gh-aw/mcp-config cat > /tmp/gh-aw/mcp-config/config.toml << EOF @@ -901,7 +914,7 @@ jobs: run: | mkdir -p $(dirname "$GITHUB_AW_PROMPT") cat > $GITHUB_AW_PROMPT << 'EOF' - Write a poem about the last 3 pull requests and publish an issue. + Write a delightful poem about the last 3 pull requests and add it as a comment to discussion #${{ github.event.inputs.discussion_number || github.event.discussion.number }}. EOF - name: Append XPIA security instructions to prompt @@ -944,13 +957,13 @@ jobs: --- - ## Creating an IssueReporting Missing Tools or Functionality + ## Adding a Comment to an Issue or Pull Request, Reporting Missing Tools or Functionality **IMPORTANT**: To do the actions mentioned in the header of this section, use the **safe-outputs** tools, do NOT attempt to use `gh`, do NOT attempt to use the GitHub API. You don't have write access to the GitHub repo. - **Creating an Issue** + **Adding a Comment to an Issue or Pull Request** - To create an issue, use the create-issue tool from the safe-outputs MCP + To add a comment to an issue or pull request, use the add-comments tool from the safe-outputs MCP **Reporting Missing Tools or Functionality** @@ -1041,7 +1054,7 @@ jobs: uses: actions/github-script@v8 env: GITHUB_AW_SAFE_OUTPUTS: ${{ env.GITHUB_AW_SAFE_OUTPUTS }} - GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"create-issue\":{\"max\":1},\"missing-tool\":{}}" + GITHUB_AW_SAFE_OUTPUTS_CONFIG: "{\"add-comment\":{\"max\":1,\"target\":\"*\"},\"missing-tool\":{}}" with: script: | async function main() { @@ -2416,7 +2429,7 @@ jobs: AGENT_OUTPUT: ${{ needs.agent.outputs.output }} WORKFLOW_NAME: "Dev" WORKFLOW_DESCRIPTION: "No description provided" - WORKFLOW_MARKDOWN: "Write a poem about the last 3 pull requests and publish an issue.\n" + WORKFLOW_MARKDOWN: "Write a delightful poem about the last 3 pull requests and add it as a comment to discussion #${{ github.event.inputs.discussion_number || github.event.discussion.number }}.\n" with: script: | const fs = require('fs'); @@ -2561,45 +2574,39 @@ jobs: path: /tmp/gh-aw/threat-detection/detection.log if-no-files-found: ignore - create_issue: + add_comment: needs: - agent - detection - if: (always()) && (contains(needs.agent.outputs.output_types, 'create-issue')) + if: (always()) && (contains(needs.agent.outputs.output_types, 'add-comment')) runs-on: ubuntu-latest permissions: contents: read issues: write + pull-requests: write + discussions: write timeout-minutes: 10 outputs: - issue_number: ${{ steps.create_issue.outputs.issue_number }} - issue_url: ${{ steps.create_issue.outputs.issue_url }} + comment_id: ${{ steps.add_comment.outputs.comment_id }} + comment_url: ${{ steps.add_comment.outputs.comment_url }} steps: - - name: Create Output Issue - id: create_issue + - name: Debug agent outputs + env: + AGENT_OUTPUT: ${{ needs.agent.outputs.output }} + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + run: | + echo "Output: $AGENT_OUTPUT" + echo "Output types: $AGENT_OUTPUT_TYPES" + - name: Add Issue Comment + id: add_comment uses: actions/github-script@v8 env: GITHUB_AW_AGENT_OUTPUT: ${{ needs.agent.outputs.output }} GITHUB_AW_WORKFLOW_NAME: "Dev" - GITHUB_AW_ISSUE_TITLE_PREFIX: "[dev] " - GITHUB_AW_ISSUE_LABELS: "dev,sub-task,poetry" + GITHUB_AW_COMMENT_TARGET: "*" GITHUB_AW_SAFE_OUTPUTS_STAGED: "true" with: script: | - function sanitizeLabelContent(content) { - if (!content || typeof content !== "string") { - 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( - /(^|[^\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}\`` - ); - sanitized = sanitized.replace(/[<>&'"]/g, ""); - return sanitized.trim(); - } function generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL) { let footer = `\n\n> AI generated by [${workflowName}](${runUrl})`; if (workflowSource && workflowSourceURL) { @@ -2631,74 +2638,122 @@ jobs: core.info("No valid items found in agent output"); return; } - const createIssueItems = validatedOutput.items.filter(item => item.type === "create-issue"); - if (createIssueItems.length === 0) { - core.info("No create-issue items found in agent output"); + const commentItems = validatedOutput.items.filter( item => item.type === "add-comment"); + if (commentItems.length === 0) { + core.info("No add-comment items found in agent output"); return; } - core.info(`Found ${createIssueItems.length} create-issue item(s)`); + core.info(`Found ${commentItems.length} add-comment item(s)`); + function getRepositoryUrl() { + const targetRepoSlug = process.env.GITHUB_AW_TARGET_REPO_SLUG; + if (targetRepoSlug) { + const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; + return `${githubServer}/${targetRepoSlug}`; + } else if (context.payload.repository) { + return context.payload.repository.html_url; + } else { + const githubServer = process.env.GITHUB_SERVER_URL || "https://github.com"; + return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; + } + } if (isStaged) { - let summaryContent = "## 🎭 Staged Mode: Create Issues Preview\n\n"; - summaryContent += "The following issues would be created if staged mode was disabled:\n\n"; - for (let i = 0; i < createIssueItems.length; i++) { - const item = createIssueItems[i]; - summaryContent += `### Issue ${i + 1}\n`; - summaryContent += `**Title:** ${item.title || "No title provided"}\n\n`; - if (item.body) { - summaryContent += `**Body:**\n${item.body}\n\n`; - } - if (item.labels && item.labels.length > 0) { - summaryContent += `**Labels:** ${item.labels.join(", ")}\n\n`; + let summaryContent = "## 🎭 Staged Mode: Add Comments Preview\n\n"; + summaryContent += "The following comments would be added if staged mode was disabled:\n\n"; + for (let i = 0; i < commentItems.length; i++) { + const item = commentItems[i]; + summaryContent += `### Comment ${i + 1}\n`; + if (item.issue_number) { + const repoUrl = getRepositoryUrl(); + const issueUrl = `${repoUrl}/issues/${item.issue_number}`; + summaryContent += `**Target Issue:** [#${item.issue_number}](${issueUrl})\n\n`; + } else { + summaryContent += `**Target:** Current issue/PR\n\n`; } + summaryContent += `**Body:**\n${item.body || "No content provided"}\n\n`; summaryContent += "---\n\n"; } await core.summary.addRaw(summaryContent).write(); - core.info("📝 Issue creation preview written to step summary"); + core.info("📝 Comment creation preview written to step summary"); return; } - const parentIssueNumber = context.payload?.issue?.number; - const labelsEnv = process.env.GITHUB_AW_ISSUE_LABELS; - let envLabels = labelsEnv - ? labelsEnv - .split(",") - .map(label => label.trim()) - .filter(label => label) - : []; - const createdIssues = []; - for (let i = 0; i < createIssueItems.length; i++) { - const createIssueItem = createIssueItems[i]; - core.info( - `Processing create-issue item ${i + 1}/${createIssueItems.length}: title=${createIssueItem.title}, bodyLength=${createIssueItem.body.length}` - ); - const effectiveParentIssueNumber = createIssueItem.parent !== undefined ? createIssueItem.parent : parentIssueNumber; - if (effectiveParentIssueNumber && createIssueItem.parent !== undefined) { - core.info(`Using explicit parent issue number from item: #${effectiveParentIssueNumber}`); - } - let labels = [...envLabels]; - if (createIssueItem.labels && Array.isArray(createIssueItem.labels)) { - labels = [...labels, ...createIssueItem.labels]; - } - labels = labels - .filter(label => !!label) - .map(label => String(label).trim()) - .filter(label => label) - .map(label => sanitizeLabelContent(label)) - .filter(label => label) - .map(label => (label.length > 64 ? label.substring(0, 64) : label)) - .filter((label, index, arr) => arr.indexOf(label) === index); - let title = createIssueItem.title ? createIssueItem.title.trim() : ""; - let bodyLines = createIssueItem.body.split("\n"); - if (!title) { - title = createIssueItem.body || "Agent Output"; - } - const titlePrefix = process.env.GITHUB_AW_ISSUE_TITLE_PREFIX; - if (titlePrefix && !title.startsWith(titlePrefix)) { - title = titlePrefix + title; - } - if (effectiveParentIssueNumber) { - core.info("Detected issue context, parent issue #" + effectiveParentIssueNumber); - bodyLines.push(`Related to #${effectiveParentIssueNumber}`); + const commentTarget = process.env.GITHUB_AW_COMMENT_TARGET || "triggering"; + core.info(`Comment target configuration: ${commentTarget}`); + const isIssueContext = context.eventName === "issues" || context.eventName === "issue_comment"; + const isPRContext = + context.eventName === "pull_request" || + context.eventName === "pull_request_review" || + context.eventName === "pull_request_review_comment"; + const isDiscussionContext = context.eventName === "discussion" || context.eventName === "discussion_comment"; + if (commentTarget === "triggering" && !isIssueContext && !isPRContext && !isDiscussionContext) { + core.info('Target is "triggering" but not running in issue, pull request, or discussion context, skipping comment creation'); + return; + } + const createdComments = []; + for (let i = 0; i < commentItems.length; i++) { + const commentItem = commentItems[i]; + core.info(`Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`); + let issueNumber; + let discussionId; + let commentEndpoint; + if (commentTarget === "*") { + if (commentItem.issue_number) { + issueNumber = parseInt(commentItem.issue_number, 10); + if (isNaN(issueNumber) || issueNumber <= 0) { + core.info(`Invalid issue number specified: ${commentItem.issue_number}`); + continue; + } + commentEndpoint = "issues"; + } else if (commentItem.discussion_number) { + issueNumber = parseInt(commentItem.discussion_number, 10); + if (isNaN(issueNumber) || issueNumber <= 0) { + core.info(`Invalid discussion number specified: ${commentItem.discussion_number}`); + continue; + } + commentEndpoint = "discussions"; + } else { + core.info('Target is "*" but no issue_number or discussion_number specified in comment item'); + continue; + } + } else if (commentTarget && commentTarget !== "triggering") { + issueNumber = parseInt(commentTarget, 10); + if (isNaN(issueNumber) || issueNumber <= 0) { + core.info(`Invalid issue number in target configuration: ${commentTarget}`); + continue; + } + commentEndpoint = "issues"; + } else { + if (isIssueContext) { + if (context.payload.issue) { + issueNumber = context.payload.issue.number; + commentEndpoint = "issues"; + } else { + core.info("Issue context detected but no issue found in payload"); + continue; + } + } else if (isPRContext) { + if (context.payload.pull_request) { + issueNumber = context.payload.pull_request.number; + commentEndpoint = "issues"; + } else { + core.info("Pull request context detected but no pull request found in payload"); + continue; + } + } else if (isDiscussionContext) { + if (context.payload.discussion) { + issueNumber = context.payload.discussion.number; + discussionId = context.payload.discussion.node_id; + commentEndpoint = "discussions"; + } else { + core.info("Discussion context detected but no discussion found in payload"); + continue; + } + } + } + if (!issueNumber) { + core.info("Could not determine issue, pull request, or discussion number"); + continue; } + let body = commentItem.body.trim(); const workflowName = process.env.GITHUB_AW_WORKFLOW_NAME || "Workflow"; const workflowSource = process.env.GITHUB_AW_WORKFLOW_SOURCE || ""; const workflowSourceURL = process.env.GITHUB_AW_WORKFLOW_SOURCE_URL || ""; @@ -2707,106 +2762,84 @@ jobs: const runUrl = context.payload.repository ? `${context.payload.repository.html_url}/actions/runs/${runId}` : `${githubServer}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; - bodyLines.push(``, ``, generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL).trimEnd(), ""); - const body = bodyLines.join("\n").trim(); - core.info(`Creating issue with title: ${title}`); - core.info(`Labels: ${labels}`); - core.info(`Body length: ${body.length}`); + body += generateFooter(workflowName, runUrl, workflowSource, workflowSourceURL); + core.info(`Creating comment on ${commentEndpoint} #${issueNumber}`); + core.info(`Comment content length: ${body.length}`); try { - const { data: issue } = await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: title, - body: body, - labels: labels, - }); - core.info("Created issue #" + issue.number + ": " + issue.html_url); - createdIssues.push(issue); - if (effectiveParentIssueNumber) { - try { - const getIssueNodeIdQuery = ` - query($owner: String!, $repo: String!, $issueNumber: Int!) { + let comment; + if (commentEndpoint === "discussions") { + if (!discussionId) { + const discussionQuery = ` + query($owner: String!, $repo: String!, $number: Int!) { repository(owner: $owner, name: $repo) { - issue(number: $issueNumber) { + discussion(number: $number) { id } } } `; - const parentResult = await github.graphql(getIssueNodeIdQuery, { - owner: context.repo.owner, - repo: context.repo.repo, - issueNumber: effectiveParentIssueNumber, - }); - const parentNodeId = parentResult.repository.issue.id; - const childResult = await github.graphql(getIssueNodeIdQuery, { + const queryResult = await github.graphql(discussionQuery, { owner: context.repo.owner, repo: context.repo.repo, - issueNumber: issue.number, + number: issueNumber, }); - const childNodeId = childResult.repository.issue.id; - const addSubIssueMutation = ` - mutation($parentId: ID!, $subIssueId: ID!) { - addSubIssue(input: { - parentId: $parentId, - subIssueId: $subIssueId - }) { - subIssue { - id - number - } + discussionId = queryResult.repository.discussion.id; + } + const addDiscussionCommentMutation = ` + mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: { + discussionId: $discussionId, + body: $body + }) { + comment { + id + url + databaseId } } - `; - await github.graphql(addSubIssueMutation, { - parentId: parentNodeId, - subIssueId: childNodeId, - }); - core.info("Linked issue #" + issue.number + " as sub-issue of #" + effectiveParentIssueNumber); - } catch (error) { - core.info(`Warning: Could not link sub-issue to parent: ${error instanceof Error ? error.message : String(error)}`); - try { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: effectiveParentIssueNumber, - body: `Created related issue: #${issue.number}`, - }); - core.info("Added comment to parent issue #" + effectiveParentIssueNumber + " (sub-issue linking not available)"); - } catch (commentError) { - core.info( - `Warning: Could not add comment to parent issue: ${commentError instanceof Error ? commentError.message : String(commentError)}` - ); } - } + `; + const mutationResult = await github.graphql(addDiscussionCommentMutation, { + discussionId: discussionId, + body: body, + }); + const discussionComment = mutationResult.addDiscussionComment.comment; + comment = { + id: discussionComment.databaseId, + html_url: discussionComment.url, + }; + core.info("Created discussion comment #" + comment.id + ": " + comment.html_url); + } else { + const { data: restComment } = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: body, + }); + comment = restComment; + core.info("Created comment #" + comment.id + ": " + comment.html_url); } - if (i === createIssueItems.length - 1) { - core.setOutput("issue_number", issue.number); - core.setOutput("issue_url", issue.html_url); + createdComments.push(comment); + if (i === commentItems.length - 1) { + core.setOutput("comment_id", comment.id); + core.setOutput("comment_url", comment.html_url); } } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes("Issues has been disabled in this repository")) { - core.info(`⚠ Cannot create issue "${title}": Issues are disabled for this repository`); - core.info("Consider enabling issues in repository settings if you want to create issues automatically"); - continue; - } - core.error(`✗ Failed to create issue "${title}": ${errorMessage}`); + core.error(`✗ Failed to create comment: ${error instanceof Error ? error.message : String(error)}`); throw error; } } - if (createdIssues.length > 0) { - let summaryContent = "\n\n## GitHub Issues\n"; - for (const issue of createdIssues) { - summaryContent += `- Issue #${issue.number}: [${issue.title}](${issue.html_url})\n`; + if (createdComments.length > 0) { + let summaryContent = "\n\n## GitHub Comments\n"; + for (const comment of createdComments) { + summaryContent += `- Comment #${comment.id}: [View Comment](${comment.html_url})\n`; } await core.summary.addRaw(summaryContent).write(); } - core.info(`Successfully created ${createdIssues.length} issue(s)`); + core.info(`Successfully created ${createdComments.length} comment(s)`); + return createdComments; } - (async () => { - await main(); - })(); + await main(); missing_tool: needs: diff --git a/.github/workflows/dev.md b/.github/workflows/dev.md index b5fd61c7a2..b3067bdc2f 100644 --- a/.github/workflows/dev.md +++ b/.github/workflows/dev.md @@ -1,14 +1,21 @@ --- on: + discussion: + types: [created] + discussion_comment: + types: [created] workflow_dispatch: - push: - paths: - - '.github/workflows/dev.lock.yml' + inputs: + discussion_number: + description: "Discussion number to add comment to" + required: true + type: string name: Dev engine: codex permissions: contents: read actions: read + discussions: write tools: github: mode: "remote" @@ -16,9 +23,8 @@ tools: - "pull_requests" safe-outputs: staged: true - create-issue: - title-prefix: "[dev] " - labels: [dev, sub-task, poetry] + add-comment: + target: "*" --- -Write a poem about the last 3 pull requests and publish an issue. +Write a delightful poem about the last 3 pull requests and add it as a comment to discussion #${{ github.event.inputs.discussion_number || github.event.discussion.number }}. diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml index 237b476753..9ab6212256 100644 --- a/.github/workflows/duplicate-code-detector.lock.yml +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -510,7 +510,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -520,6 +520,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/go-pattern-detector.lock.yml b/.github/workflows/go-pattern-detector.lock.yml index 6abff25f08..99a9e56275 100644 --- a/.github/workflows/go-pattern-detector.lock.yml +++ b/.github/workflows/go-pattern-detector.lock.yml @@ -607,7 +607,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -617,6 +617,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/issue-classifier.lock.yml b/.github/workflows/issue-classifier.lock.yml index 745a52b33d..19476cbe64 100644 --- a/.github/workflows/issue-classifier.lock.yml +++ b/.github/workflows/issue-classifier.lock.yml @@ -817,7 +817,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -827,6 +827,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/notion-issue-summary.lock.yml b/.github/workflows/notion-issue-summary.lock.yml index 2f3e117623..06f585c2fa 100644 --- a/.github/workflows/notion-issue-summary.lock.yml +++ b/.github/workflows/notion-issue-summary.lock.yml @@ -762,7 +762,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -772,6 +772,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/pdf-summary.lock.yml b/.github/workflows/pdf-summary.lock.yml index 7e710e97c6..a8afa61b9a 100644 --- a/.github/workflows/pdf-summary.lock.yml +++ b/.github/workflows/pdf-summary.lock.yml @@ -873,7 +873,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -883,6 +883,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, @@ -3601,13 +3605,14 @@ jobs: - agent - detection if: > - ((always()) && (contains(needs.agent.outputs.output_types, 'add-comment'))) && ((github.event.issue.number) || - (github.event.pull_request.number)) + ((always()) && (contains(needs.agent.outputs.output_types, 'add-comment'))) && (github.event.issue.number || + github.event.pull_request.number || github.event.discussion.number) runs-on: ubuntu-latest permissions: contents: read issues: write pull-requests: write + discussions: write timeout-minutes: 10 outputs: comment_id: ${{ steps.add_comment.outputs.comment_id }} @@ -3704,8 +3709,9 @@ jobs: context.eventName === "pull_request" || context.eventName === "pull_request_review" || context.eventName === "pull_request_review_comment"; - if (commentTarget === "triggering" && !isIssueContext && !isPRContext) { - core.info('Target is "triggering" but not running in issue or pull request context, skipping comment creation'); + const isDiscussionContext = context.eventName === "discussion" || context.eventName === "discussion_comment"; + if (commentTarget === "triggering" && !isIssueContext && !isPRContext && !isDiscussionContext) { + core.info('Target is "triggering" but not running in issue, pull request, or discussion context, skipping comment creation'); return; } const createdComments = []; @@ -3713,6 +3719,7 @@ jobs: const commentItem = commentItems[i]; core.info(`Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`); let issueNumber; + let discussionId; let commentEndpoint; if (commentTarget === "*") { if (commentItem.issue_number) { @@ -3722,8 +3729,15 @@ jobs: continue; } commentEndpoint = "issues"; + } else if (commentItem.discussion_number) { + issueNumber = parseInt(commentItem.discussion_number, 10); + if (isNaN(issueNumber) || issueNumber <= 0) { + core.info(`Invalid discussion number specified: ${commentItem.discussion_number}`); + continue; + } + commentEndpoint = "discussions"; } else { - core.info('Target is "*" but no issue_number specified in comment item'); + core.info('Target is "*" but no issue_number or discussion_number specified in comment item'); continue; } } else if (commentTarget && commentTarget !== "triggering") { @@ -3750,10 +3764,19 @@ jobs: core.info("Pull request context detected but no pull request found in payload"); continue; } + } else if (isDiscussionContext) { + if (context.payload.discussion) { + issueNumber = context.payload.discussion.number; + discussionId = context.payload.discussion.node_id; + commentEndpoint = "discussions"; + } else { + core.info("Discussion context detected but no discussion found in payload"); + continue; + } } } if (!issueNumber) { - core.info("Could not determine issue or pull request number"); + core.info("Could not determine issue, pull request, or discussion number"); continue; } let body = commentItem.body.trim(); @@ -3769,13 +3792,59 @@ jobs: core.info(`Creating comment on ${commentEndpoint} #${issueNumber}`); core.info(`Comment content length: ${body.length}`); try { - const { data: comment } = await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body: body, - }); - core.info("Created comment #" + comment.id + ": " + comment.html_url); + let comment; + if (commentEndpoint === "discussions") { + if (!discussionId) { + const discussionQuery = ` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + discussion(number: $number) { + id + } + } + } + `; + const queryResult = await github.graphql(discussionQuery, { + owner: context.repo.owner, + repo: context.repo.repo, + number: issueNumber, + }); + discussionId = queryResult.repository.discussion.id; + } + const addDiscussionCommentMutation = ` + mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: { + discussionId: $discussionId, + body: $body + }) { + comment { + id + url + databaseId + } + } + } + `; + const mutationResult = await github.graphql(addDiscussionCommentMutation, { + discussionId: discussionId, + body: body, + }); + const discussionComment = mutationResult.addDiscussionComment.comment; + comment = { + id: discussionComment.databaseId, + html_url: discussionComment.url, + }; + core.info("Created discussion comment #" + comment.id + ": " + comment.html_url); + } else { + const { data: restComment } = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: body, + }); + comment = restComment; + core.info("Created comment #" + comment.id + ": " + comment.html_url); + } createdComments.push(comment); if (i === commentItems.length - 1) { core.setOutput("comment_id", comment.id); diff --git a/.github/workflows/plan.lock.yml b/.github/workflows/plan.lock.yml index 167eec3a3b..737960e4a6 100644 --- a/.github/workflows/plan.lock.yml +++ b/.github/workflows/plan.lock.yml @@ -828,7 +828,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -838,6 +838,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index c419e0c4f6..27bc1c2cd8 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -858,7 +858,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -868,6 +868,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, @@ -3923,6 +3927,7 @@ jobs: contents: read issues: write pull-requests: write + discussions: write timeout-minutes: 10 outputs: comment_id: ${{ steps.add_comment.outputs.comment_id }} @@ -4021,8 +4026,9 @@ jobs: context.eventName === "pull_request" || context.eventName === "pull_request_review" || context.eventName === "pull_request_review_comment"; - if (commentTarget === "triggering" && !isIssueContext && !isPRContext) { - core.info('Target is "triggering" but not running in issue or pull request context, skipping comment creation'); + const isDiscussionContext = context.eventName === "discussion" || context.eventName === "discussion_comment"; + if (commentTarget === "triggering" && !isIssueContext && !isPRContext && !isDiscussionContext) { + core.info('Target is "triggering" but not running in issue, pull request, or discussion context, skipping comment creation'); return; } const createdComments = []; @@ -4030,6 +4036,7 @@ jobs: const commentItem = commentItems[i]; core.info(`Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`); let issueNumber; + let discussionId; let commentEndpoint; if (commentTarget === "*") { if (commentItem.issue_number) { @@ -4039,8 +4046,15 @@ jobs: continue; } commentEndpoint = "issues"; + } else if (commentItem.discussion_number) { + issueNumber = parseInt(commentItem.discussion_number, 10); + if (isNaN(issueNumber) || issueNumber <= 0) { + core.info(`Invalid discussion number specified: ${commentItem.discussion_number}`); + continue; + } + commentEndpoint = "discussions"; } else { - core.info('Target is "*" but no issue_number specified in comment item'); + core.info('Target is "*" but no issue_number or discussion_number specified in comment item'); continue; } } else if (commentTarget && commentTarget !== "triggering") { @@ -4067,10 +4081,19 @@ jobs: core.info("Pull request context detected but no pull request found in payload"); continue; } + } else if (isDiscussionContext) { + if (context.payload.discussion) { + issueNumber = context.payload.discussion.number; + discussionId = context.payload.discussion.node_id; + commentEndpoint = "discussions"; + } else { + core.info("Discussion context detected but no discussion found in payload"); + continue; + } } } if (!issueNumber) { - core.info("Could not determine issue or pull request number"); + core.info("Could not determine issue, pull request, or discussion number"); continue; } let body = commentItem.body.trim(); @@ -4086,13 +4109,59 @@ jobs: core.info(`Creating comment on ${commentEndpoint} #${issueNumber}`); core.info(`Comment content length: ${body.length}`); try { - const { data: comment } = await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body: body, - }); - core.info("Created comment #" + comment.id + ": " + comment.html_url); + let comment; + if (commentEndpoint === "discussions") { + if (!discussionId) { + const discussionQuery = ` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + discussion(number: $number) { + id + } + } + } + `; + const queryResult = await github.graphql(discussionQuery, { + owner: context.repo.owner, + repo: context.repo.repo, + number: issueNumber, + }); + discussionId = queryResult.repository.discussion.id; + } + const addDiscussionCommentMutation = ` + mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: { + discussionId: $discussionId, + body: $body + }) { + comment { + id + url + databaseId + } + } + } + `; + const mutationResult = await github.graphql(addDiscussionCommentMutation, { + discussionId: discussionId, + body: body, + }); + const discussionComment = mutationResult.addDiscussionComment.comment; + comment = { + id: discussionComment.databaseId, + html_url: discussionComment.url, + }; + core.info("Created discussion comment #" + comment.id + ": " + comment.html_url); + } else { + const { data: restComment } = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: body, + }); + comment = restComment; + core.info("Created comment #" + comment.id + ": " + comment.html_url); + } createdComments.push(comment); if (i === commentItems.length - 1) { core.setOutput("comment_id", comment.id); diff --git a/.github/workflows/repo-tree-map.lock.yml b/.github/workflows/repo-tree-map.lock.yml index bfd1216080..26a131d901 100644 --- a/.github/workflows/repo-tree-map.lock.yml +++ b/.github/workflows/repo-tree-map.lock.yml @@ -495,7 +495,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -505,6 +505,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/scout.lock.yml b/.github/workflows/scout.lock.yml index 8e83260cf1..7910faffbb 100644 --- a/.github/workflows/scout.lock.yml +++ b/.github/workflows/scout.lock.yml @@ -1139,7 +1139,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -1149,6 +1149,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, @@ -3935,13 +3939,14 @@ jobs: - agent - detection if: > - ((always()) && (contains(needs.agent.outputs.output_types, 'add-comment'))) && ((github.event.issue.number) || - (github.event.pull_request.number)) + ((always()) && (contains(needs.agent.outputs.output_types, 'add-comment'))) && (github.event.issue.number || + github.event.pull_request.number || github.event.discussion.number) runs-on: ubuntu-latest permissions: contents: read issues: write pull-requests: write + discussions: write timeout-minutes: 10 outputs: comment_id: ${{ steps.add_comment.outputs.comment_id }} @@ -4038,8 +4043,9 @@ jobs: context.eventName === "pull_request" || context.eventName === "pull_request_review" || context.eventName === "pull_request_review_comment"; - if (commentTarget === "triggering" && !isIssueContext && !isPRContext) { - core.info('Target is "triggering" but not running in issue or pull request context, skipping comment creation'); + const isDiscussionContext = context.eventName === "discussion" || context.eventName === "discussion_comment"; + if (commentTarget === "triggering" && !isIssueContext && !isPRContext && !isDiscussionContext) { + core.info('Target is "triggering" but not running in issue, pull request, or discussion context, skipping comment creation'); return; } const createdComments = []; @@ -4047,6 +4053,7 @@ jobs: const commentItem = commentItems[i]; core.info(`Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`); let issueNumber; + let discussionId; let commentEndpoint; if (commentTarget === "*") { if (commentItem.issue_number) { @@ -4056,8 +4063,15 @@ jobs: continue; } commentEndpoint = "issues"; + } else if (commentItem.discussion_number) { + issueNumber = parseInt(commentItem.discussion_number, 10); + if (isNaN(issueNumber) || issueNumber <= 0) { + core.info(`Invalid discussion number specified: ${commentItem.discussion_number}`); + continue; + } + commentEndpoint = "discussions"; } else { - core.info('Target is "*" but no issue_number specified in comment item'); + core.info('Target is "*" but no issue_number or discussion_number specified in comment item'); continue; } } else if (commentTarget && commentTarget !== "triggering") { @@ -4084,10 +4098,19 @@ jobs: core.info("Pull request context detected but no pull request found in payload"); continue; } + } else if (isDiscussionContext) { + if (context.payload.discussion) { + issueNumber = context.payload.discussion.number; + discussionId = context.payload.discussion.node_id; + commentEndpoint = "discussions"; + } else { + core.info("Discussion context detected but no discussion found in payload"); + continue; + } } } if (!issueNumber) { - core.info("Could not determine issue or pull request number"); + core.info("Could not determine issue, pull request, or discussion number"); continue; } let body = commentItem.body.trim(); @@ -4103,13 +4126,59 @@ jobs: core.info(`Creating comment on ${commentEndpoint} #${issueNumber}`); core.info(`Comment content length: ${body.length}`); try { - const { data: comment } = await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body: body, - }); - core.info("Created comment #" + comment.id + ": " + comment.html_url); + let comment; + if (commentEndpoint === "discussions") { + if (!discussionId) { + const discussionQuery = ` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + discussion(number: $number) { + id + } + } + } + `; + const queryResult = await github.graphql(discussionQuery, { + owner: context.repo.owner, + repo: context.repo.repo, + number: issueNumber, + }); + discussionId = queryResult.repository.discussion.id; + } + const addDiscussionCommentMutation = ` + mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: { + discussionId: $discussionId, + body: $body + }) { + comment { + id + url + databaseId + } + } + } + `; + const mutationResult = await github.graphql(addDiscussionCommentMutation, { + discussionId: discussionId, + body: body, + }); + const discussionComment = mutationResult.addDiscussionComment.comment; + comment = { + id: discussionComment.databaseId, + html_url: discussionComment.url, + }; + core.info("Created discussion comment #" + comment.id + ": " + comment.html_url); + } else { + const { data: restComment } = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: body, + }); + comment = restComment; + core.info("Created comment #" + comment.id + ": " + comment.html_url); + } createdComments.push(comment); if (i === commentItems.length - 1) { core.setOutput("comment_id", comment.id); diff --git a/.github/workflows/security-fix-pr.lock.yml b/.github/workflows/security-fix-pr.lock.yml index d0815d3564..721d6227f4 100644 --- a/.github/workflows/security-fix-pr.lock.yml +++ b/.github/workflows/security-fix-pr.lock.yml @@ -599,7 +599,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -609,6 +609,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 3a67e40852..c0c14c6ce0 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -596,7 +596,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -606,6 +606,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 7cd80ecbaa..be40d35816 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -488,7 +488,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -498,6 +498,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 9e7b161370..cb2e2fa3e3 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -490,7 +490,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -500,6 +500,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/smoke-genaiscript.lock.yml b/.github/workflows/smoke-genaiscript.lock.yml index 14336ad5fa..a53a1dbf8b 100644 --- a/.github/workflows/smoke-genaiscript.lock.yml +++ b/.github/workflows/smoke-genaiscript.lock.yml @@ -493,7 +493,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -503,6 +503,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 32e8d3e9d1..5489d12303 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -635,7 +635,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -645,6 +645,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, @@ -3132,13 +3136,14 @@ jobs: - agent - detection if: > - ((always()) && (contains(needs.agent.outputs.output_types, 'add-comment'))) && ((github.event.issue.number) || - (github.event.pull_request.number)) + ((always()) && (contains(needs.agent.outputs.output_types, 'add-comment'))) && (github.event.issue.number || + github.event.pull_request.number || github.event.discussion.number) runs-on: ubuntu-latest permissions: contents: read issues: write pull-requests: write + discussions: write timeout-minutes: 10 outputs: comment_id: ${{ steps.add_comment.outputs.comment_id }} @@ -3235,8 +3240,9 @@ jobs: context.eventName === "pull_request" || context.eventName === "pull_request_review" || context.eventName === "pull_request_review_comment"; - if (commentTarget === "triggering" && !isIssueContext && !isPRContext) { - core.info('Target is "triggering" but not running in issue or pull request context, skipping comment creation'); + const isDiscussionContext = context.eventName === "discussion" || context.eventName === "discussion_comment"; + if (commentTarget === "triggering" && !isIssueContext && !isPRContext && !isDiscussionContext) { + core.info('Target is "triggering" but not running in issue, pull request, or discussion context, skipping comment creation'); return; } const createdComments = []; @@ -3244,6 +3250,7 @@ jobs: const commentItem = commentItems[i]; core.info(`Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`); let issueNumber; + let discussionId; let commentEndpoint; if (commentTarget === "*") { if (commentItem.issue_number) { @@ -3253,8 +3260,15 @@ jobs: continue; } commentEndpoint = "issues"; + } else if (commentItem.discussion_number) { + issueNumber = parseInt(commentItem.discussion_number, 10); + if (isNaN(issueNumber) || issueNumber <= 0) { + core.info(`Invalid discussion number specified: ${commentItem.discussion_number}`); + continue; + } + commentEndpoint = "discussions"; } else { - core.info('Target is "*" but no issue_number specified in comment item'); + core.info('Target is "*" but no issue_number or discussion_number specified in comment item'); continue; } } else if (commentTarget && commentTarget !== "triggering") { @@ -3281,10 +3295,19 @@ jobs: core.info("Pull request context detected but no pull request found in payload"); continue; } + } else if (isDiscussionContext) { + if (context.payload.discussion) { + issueNumber = context.payload.discussion.number; + discussionId = context.payload.discussion.node_id; + commentEndpoint = "discussions"; + } else { + core.info("Discussion context detected but no discussion found in payload"); + continue; + } } } if (!issueNumber) { - core.info("Could not determine issue or pull request number"); + core.info("Could not determine issue, pull request, or discussion number"); continue; } let body = commentItem.body.trim(); @@ -3300,13 +3323,59 @@ jobs: core.info(`Creating comment on ${commentEndpoint} #${issueNumber}`); core.info(`Comment content length: ${body.length}`); try { - const { data: comment } = await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body: body, - }); - core.info("Created comment #" + comment.id + ": " + comment.html_url); + let comment; + if (commentEndpoint === "discussions") { + if (!discussionId) { + const discussionQuery = ` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + discussion(number: $number) { + id + } + } + } + `; + const queryResult = await github.graphql(discussionQuery, { + owner: context.repo.owner, + repo: context.repo.repo, + number: issueNumber, + }); + discussionId = queryResult.repository.discussion.id; + } + const addDiscussionCommentMutation = ` + mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: { + discussionId: $discussionId, + body: $body + }) { + comment { + id + url + databaseId + } + } + } + `; + const mutationResult = await github.graphql(addDiscussionCommentMutation, { + discussionId: discussionId, + body: body, + }); + const discussionComment = mutationResult.addDiscussionComment.comment; + comment = { + id: discussionComment.databaseId, + html_url: discussionComment.url, + }; + core.info("Created discussion comment #" + comment.id + ": " + comment.html_url); + } else { + const { data: restComment } = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: body, + }); + comment = restComment; + core.info("Created comment #" + comment.id + ": " + comment.html_url); + } createdComments.push(comment); if (i === commentItems.length - 1) { core.setOutput("comment_id", comment.id); diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index ac0a114bef..525bbc46be 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -701,7 +701,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -711,6 +711,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 4b0ad722c4..0bc41e474d 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -788,7 +788,7 @@ jobs: }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -798,6 +798,10 @@ jobs: type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, @@ -3289,13 +3293,14 @@ jobs: - agent - detection if: > - ((always()) && (contains(needs.agent.outputs.output_types, 'add-comment'))) && ((github.event.issue.number) || - (github.event.pull_request.number)) + ((always()) && (contains(needs.agent.outputs.output_types, 'add-comment'))) && (github.event.issue.number || + github.event.pull_request.number || github.event.discussion.number) runs-on: ubuntu-latest permissions: contents: read issues: write pull-requests: write + discussions: write timeout-minutes: 10 outputs: comment_id: ${{ steps.add_comment.outputs.comment_id }} @@ -3392,8 +3397,9 @@ jobs: context.eventName === "pull_request" || context.eventName === "pull_request_review" || context.eventName === "pull_request_review_comment"; - if (commentTarget === "triggering" && !isIssueContext && !isPRContext) { - core.info('Target is "triggering" but not running in issue or pull request context, skipping comment creation'); + const isDiscussionContext = context.eventName === "discussion" || context.eventName === "discussion_comment"; + if (commentTarget === "triggering" && !isIssueContext && !isPRContext && !isDiscussionContext) { + core.info('Target is "triggering" but not running in issue, pull request, or discussion context, skipping comment creation'); return; } const createdComments = []; @@ -3401,6 +3407,7 @@ jobs: const commentItem = commentItems[i]; core.info(`Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`); let issueNumber; + let discussionId; let commentEndpoint; if (commentTarget === "*") { if (commentItem.issue_number) { @@ -3410,8 +3417,15 @@ jobs: continue; } commentEndpoint = "issues"; + } else if (commentItem.discussion_number) { + issueNumber = parseInt(commentItem.discussion_number, 10); + if (isNaN(issueNumber) || issueNumber <= 0) { + core.info(`Invalid discussion number specified: ${commentItem.discussion_number}`); + continue; + } + commentEndpoint = "discussions"; } else { - core.info('Target is "*" but no issue_number specified in comment item'); + core.info('Target is "*" but no issue_number or discussion_number specified in comment item'); continue; } } else if (commentTarget && commentTarget !== "triggering") { @@ -3438,10 +3452,19 @@ jobs: core.info("Pull request context detected but no pull request found in payload"); continue; } + } else if (isDiscussionContext) { + if (context.payload.discussion) { + issueNumber = context.payload.discussion.number; + discussionId = context.payload.discussion.node_id; + commentEndpoint = "discussions"; + } else { + core.info("Discussion context detected but no discussion found in payload"); + continue; + } } } if (!issueNumber) { - core.info("Could not determine issue or pull request number"); + core.info("Could not determine issue, pull request, or discussion number"); continue; } let body = commentItem.body.trim(); @@ -3457,13 +3480,59 @@ jobs: core.info(`Creating comment on ${commentEndpoint} #${issueNumber}`); core.info(`Comment content length: ${body.length}`); try { - const { data: comment } = await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body: body, - }); - core.info("Created comment #" + comment.id + ": " + comment.html_url); + let comment; + if (commentEndpoint === "discussions") { + if (!discussionId) { + const discussionQuery = ` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + discussion(number: $number) { + id + } + } + } + `; + const queryResult = await github.graphql(discussionQuery, { + owner: context.repo.owner, + repo: context.repo.repo, + number: issueNumber, + }); + discussionId = queryResult.repository.discussion.id; + } + const addDiscussionCommentMutation = ` + mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: { + discussionId: $discussionId, + body: $body + }) { + comment { + id + url + databaseId + } + } + } + `; + const mutationResult = await github.graphql(addDiscussionCommentMutation, { + discussionId: discussionId, + body: body, + }); + const discussionComment = mutationResult.addDiscussionComment.comment; + comment = { + id: discussionComment.databaseId, + html_url: discussionComment.url, + }; + core.info("Created discussion comment #" + comment.id + ": " + comment.html_url); + } else { + const { data: restComment } = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: body, + }); + comment = restComment; + core.info("Created comment #" + comment.id + ": " + comment.html_url); + } createdComments.push(comment); if (i === commentItems.length - 1) { core.setOutput("comment_id", comment.id); diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index f3ca45aa0e..e694074b93 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -84,7 +84,7 @@ The compiled workflow will have additional prompting describing that, to create ### Issue Comment Creation (`add-comment:`) -Adding comment creation to the `safe-outputs:` section declares that the workflow should conclude with posting comments based on the workflow's output. By default, comments are posted on the triggering issue or pull request, but this can be configured using the `target` option. +Adding comment creation to the `safe-outputs:` section declares that the workflow should conclude with posting comments based on the workflow's output. By default, comments are posted on the triggering issue, pull request, or discussion, but this can be configured using the `target` option. **Basic Configuration:** ```yaml @@ -98,9 +98,9 @@ safe-outputs: add-comment: max: 3 # Optional: maximum number of comments (default: 1) target: "*" # Optional: target for comments - # "triggering" (default) - only comment on triggering issue/PR - # "*" - allow comments on any issue (requires issue_number in agent output) - # explicit number - comment on specific issue number + # "triggering" (default) - only comment on triggering issue/PR/discussion + # "*" - allow comments on any issue/discussion (requires issue_number or discussion_number in agent output) + # explicit number - comment on specific issue/discussion number target-repo: "owner/target-repo" # Optional: create comments in a different repository (requires github-token with appropriate permissions) ``` @@ -122,14 +122,40 @@ safe-outputs: max: 3 --- -# Issue/PR Analysis Agent +# Issue/PR/Discussion Analysis Agent -Analyze the issue or pull request and provide feedback. -Create issue comments on the triggering issue or PR with your analysis findings. Each comment should provide specific insights about different aspects of the issue. +Analyze the issue, pull request, or discussion and provide feedback. +Create comments on the triggering issue, PR, or discussion with your analysis findings. Each comment should provide specific insights about different aspects of the content. ``` The compiled workflow will have additional prompting describing that, to create comments, it should write the comment content to a special file. +**Discussion Support:** + +Comments can also be added to GitHub discussions. When triggered by a discussion event, the workflow will use GraphQL to create discussion comments: + +```aw wrap +--- +on: + discussion: + types: [created] +permissions: + contents: read + discussions: write + actions: read +engine: claude +safe-outputs: + add-comment: +--- + +# Discussion Response Agent + +Analyze the discussion and provide a helpful response. +Create a comment on the discussion with your insights. +``` + +**Note:** Discussion events require the `discussions: write` permission. The add-comment job automatically includes this permission. + ### Add Issue Label (`add-labels:`) Adding `add-labels:` to the `safe-outputs:` section of your workflow declares that the workflow should conclude with adding labels to issues or pull requests based on the coding agent's analysis. By default, labels are added to the triggering issue or pull request, but this can be configured using the `target` option. diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index ef942e587b..9dec97c91f 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -67,6 +67,7 @@ var AllowedExpressions = []string{ "github.event.comment.id", "github.event.deployment.id", "github.event.deployment_status.id", + "github.event.discussion.number", "github.event.head_commit.id", "github.event.installation.id", "github.event.issue.number", diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index f0b5e81fe2..aeb92f28a5 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -270,6 +270,54 @@ } } }, + "discussion": { + "description": "Discussion event trigger that runs when a discussion is created, edited, or otherwise modified", + "type": "object", + "additionalProperties": false, + "properties": { + "types": { + "type": "array", + "description": "Types of discussion events", + "items": { + "type": "string", + "enum": [ + "created", + "edited", + "deleted", + "transferred", + "pinned", + "unpinned", + "labeled", + "unlabeled", + "locked", + "unlocked", + "category_changed", + "answered", + "unanswered" + ] + } + } + } + }, + "discussion_comment": { + "description": "Discussion comment event trigger that runs when a comment on a discussion is created, edited, or deleted", + "type": "object", + "additionalProperties": false, + "properties": { + "types": { + "type": "array", + "description": "Types of discussion comment events", + "items": { + "type": "string", + "enum": [ + "created", + "edited", + "deleted" + ] + } + } + } + }, "schedule": { "type": "array", "description": "Scheduled trigger events", @@ -1618,7 +1666,7 @@ "oneOf": [ { "type": "object", - "description": "Configuration for automatically creating GitHub issue or pull request comments from AI workflow output. The main job does not need write permissions.", + "description": "Configuration for automatically creating GitHub issue, pull request, or discussion comments from AI workflow output. The main job does not need write permissions.", "properties": { "max": { "type": "integer", @@ -1634,7 +1682,7 @@ }, "target": { "type": "string", - "description": "Target for comments: 'triggering' (default), '*' (any issue), or explicit issue number" + "description": "Target for comments: 'triggering' (default), '*' (any issue/discussion), or explicit issue/discussion number" }, "target-repo": { "type": "string", @@ -1649,7 +1697,7 @@ }, { "type": "null", - "description": "Enable issue comment creation with default configuration" + "description": "Enable issue, pull request, and discussion comment creation with default configuration" } ] }, diff --git a/pkg/workflow/add_comment.go b/pkg/workflow/add_comment.go index 93c05bffd2..7161cfd3c8 100644 --- a/pkg/workflow/add_comment.go +++ b/pkg/workflow/add_comment.go @@ -85,6 +85,7 @@ func (c *Compiler) buildCreateOutputAddCommentJob(data *WorkflowData, mainJobNam eventCondition := buildOr( BuildPropertyAccess("github.event.issue.number"), BuildPropertyAccess("github.event.pull_request.number"), + BuildPropertyAccess("github.event.discussion.number"), ) jobCondition = buildAnd(jobCondition, eventCondition) } @@ -93,7 +94,7 @@ func (c *Compiler) buildCreateOutputAddCommentJob(data *WorkflowData, mainJobNam Name: "add_comment", If: jobCondition.Render(), RunsOn: c.formatSafeOutputsRunsOn(data.SafeOutputs), - Permissions: "permissions:\n contents: read\n issues: write\n pull-requests: write", + Permissions: "permissions:\n contents: read\n issues: write\n pull-requests: write\n discussions: write", TimeoutMinutes: 10, // 10-minute timeout as required Steps: steps, Outputs: outputs, diff --git a/pkg/workflow/add_comment_discussion_test.go b/pkg/workflow/add_comment_discussion_test.go new file mode 100644 index 0000000000..ec3f2590f1 --- /dev/null +++ b/pkg/workflow/add_comment_discussion_test.go @@ -0,0 +1,76 @@ +package workflow + +import ( + "os" + "strings" + "testing" +) + +func TestAddCommentWithDiscussionSupport(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "add-comment-discussion-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // Test case with add-comment configuration and issues trigger + // Note: We test with issues trigger since discussion events may not be fully supported yet + // but the add-comment job should now include discussion support in its conditional + testContent := `--- +on: + issues: + types: [opened] +permissions: + contents: read + issues: write + discussions: write +engine: claude +safe-outputs: + add-comment: +--- + +# Test Add Comment with Discussion + +This workflow tests add-comment support for discussions. +` + + // Write test workflow file + testFile := tmpDir + "/test-workflow.md" + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + // Create compiler and compile + compiler := NewCompiler(false, "", "test") + err = compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read generated lock file + lockFilePath := tmpDir + "/test-workflow.lock.yml" + lockContent, err := os.ReadFile(lockFilePath) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + lockContentStr := string(lockContent) + + // Verify add_comment job includes discussion permission + if !strings.Contains(lockContentStr, "discussions: write") { + t.Error("Expected discussions: write permission in add_comment job") + } + + // Verify job has conditional execution including discussion.number + expectedConditionParts := []string{ + "github.event.issue.number", + "github.event.pull_request.number", + "github.event.discussion.number", + } + for _, part := range expectedConditionParts { + if !strings.Contains(lockContentStr, part) { + t.Errorf("Expected add_comment job condition to include '%s'", part) + } + } +} diff --git a/pkg/workflow/expressions.go b/pkg/workflow/expressions.go index 6a713a8c6b..2935eb371f 100644 --- a/pkg/workflow/expressions.go +++ b/pkg/workflow/expressions.go @@ -205,8 +205,22 @@ func buildConditionTree(existingCondition string, draftCondition string) Conditi return &AndNode{Left: existingNode, Right: draftNode} } -func buildOr(left ConditionNode, right ConditionNode) ConditionNode { - return &OrNode{Left: left, Right: right} +// buildOr creates an OR condition from two or more condition nodes. +// When called with 2 arguments, it creates a simple OR node. +// When called with more arguments, it creates a DisjunctionNode to avoid deep nesting. +func buildOr(conditions ...ConditionNode) ConditionNode { + if len(conditions) == 0 { + return nil + } + if len(conditions) == 1 { + return conditions[0] + } + if len(conditions) == 2 { + return &OrNode{Left: conditions[0], Right: conditions[1]} + } + + // Use DisjunctionNode for 3+ conditions to avoid deep nesting + return &DisjunctionNode{Terms: conditions} } func buildAnd(left ConditionNode, right ConditionNode) ConditionNode { diff --git a/pkg/workflow/expressions_test.go b/pkg/workflow/expressions_test.go index b7bceadfd1..fe5fa306d8 100644 --- a/pkg/workflow/expressions_test.go +++ b/pkg/workflow/expressions_test.go @@ -722,6 +722,86 @@ func TestConvenienceHelpers(t *testing.T) { }) } +// TestBuildOr tests the variadic buildOr function +func TestBuildOr(t *testing.T) { + tests := []struct { + name string + conditions []ConditionNode + expected string + }{ + { + name: "nil conditions", + conditions: nil, + expected: "", + }, + { + name: "empty conditions", + conditions: []ConditionNode{}, + expected: "", + }, + { + name: "single condition", + conditions: []ConditionNode{ + BuildPropertyAccess("github.event.issue.number"), + }, + expected: "github.event.issue.number", + }, + { + name: "two conditions", + conditions: []ConditionNode{ + BuildPropertyAccess("github.event.issue.number"), + BuildPropertyAccess("github.event.pull_request.number"), + }, + expected: "(github.event.issue.number) || (github.event.pull_request.number)", + }, + { + name: "three conditions", + conditions: []ConditionNode{ + BuildPropertyAccess("github.event.issue.number"), + BuildPropertyAccess("github.event.pull_request.number"), + BuildPropertyAccess("github.event.discussion.number"), + }, + expected: "github.event.issue.number || github.event.pull_request.number || github.event.discussion.number", + }, + { + name: "four conditions", + conditions: []ConditionNode{ + &ExpressionNode{Expression: "a"}, + &ExpressionNode{Expression: "b"}, + &ExpressionNode{Expression: "c"}, + &ExpressionNode{Expression: "d"}, + }, + expected: "a || b || c || d", + }, + { + name: "mixed node types", + conditions: []ConditionNode{ + BuildActionEquals("opened"), + BuildEventTypeEquals("pull_request"), + BuildPropertyAccess("github.event.issue.number"), + }, + expected: "github.event.action == 'opened' || github.event_name == 'pull_request' || github.event.issue.number", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := buildOr(tt.conditions...) + if result == nil { + if tt.expected != "" { + t.Errorf("Expected '%s', got nil", tt.expected) + } + return + } + + rendered := result.Render() + if rendered != tt.expected { + t.Errorf("Expected '%s', got '%s'", tt.expected, rendered) + } + }) + } +} + // TestRealWorldExpressionPatterns tests common expression patterns used in GitHub Actions func TestRealWorldExpressionPatterns(t *testing.T) { tests := []struct { diff --git a/pkg/workflow/js/add_comment.cjs b/pkg/workflow/js/add_comment.cjs index 26ebab9214..cb614f8bc8 100644 --- a/pkg/workflow/js/add_comment.cjs +++ b/pkg/workflow/js/add_comment.cjs @@ -106,16 +106,17 @@ async function main() { const commentTarget = process.env.GITHUB_AW_COMMENT_TARGET || "triggering"; core.info(`Comment target configuration: ${commentTarget}`); - // Check if we're in an issue or pull request context + // Check if we're in an issue, pull request, or discussion context const isIssueContext = context.eventName === "issues" || context.eventName === "issue_comment"; const isPRContext = context.eventName === "pull_request" || context.eventName === "pull_request_review" || context.eventName === "pull_request_review_comment"; + const isDiscussionContext = context.eventName === "discussion" || context.eventName === "discussion_comment"; // Validate context based on target configuration - if (commentTarget === "triggering" && !isIssueContext && !isPRContext) { - core.info('Target is "triggering" but not running in issue or pull request context, skipping comment creation'); + if (commentTarget === "triggering" && !isIssueContext && !isPRContext && !isDiscussionContext) { + core.info('Target is "triggering" but not running in issue, pull request, or discussion context, skipping comment creation'); return; } @@ -126,8 +127,9 @@ async function main() { const commentItem = commentItems[i]; core.info(`Processing add-comment item ${i + 1}/${commentItems.length}: bodyLength=${commentItem.body.length}`); - // Determine the issue/PR number and comment endpoint for this comment + // Determine the issue/PR/discussion number and comment endpoint for this comment let issueNumber; + let discussionId; // GraphQL ID for discussions let commentEndpoint; if (commentTarget === "*") { @@ -139,8 +141,15 @@ async function main() { continue; } commentEndpoint = "issues"; + } else if (commentItem.discussion_number) { + issueNumber = parseInt(commentItem.discussion_number, 10); + if (isNaN(issueNumber) || issueNumber <= 0) { + core.info(`Invalid discussion number specified: ${commentItem.discussion_number}`); + continue; + } + commentEndpoint = "discussions"; } else { - core.info('Target is "*" but no issue_number specified in comment item'); + core.info('Target is "*" but no issue_number or discussion_number specified in comment item'); continue; } } else if (commentTarget && commentTarget !== "triggering") { @@ -152,7 +161,7 @@ async function main() { } commentEndpoint = "issues"; } else { - // Default behavior: use triggering issue/PR + // Default behavior: use triggering issue/PR/discussion if (isIssueContext) { if (context.payload.issue) { issueNumber = context.payload.issue.number; @@ -169,11 +178,20 @@ async function main() { core.info("Pull request context detected but no pull request found in payload"); continue; } + } else if (isDiscussionContext) { + if (context.payload.discussion) { + issueNumber = context.payload.discussion.number; + discussionId = context.payload.discussion.node_id; + commentEndpoint = "discussions"; + } else { + core.info("Discussion context detected but no discussion found in payload"); + continue; + } } } if (!issueNumber) { - core.info("Could not determine issue or pull request number"); + core.info("Could not determine issue, pull request, or discussion number"); continue; } @@ -194,15 +212,67 @@ async function main() { core.info(`Comment content length: ${body.length}`); try { - // Create the comment using GitHub API - const { data: comment } = await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - body: body, - }); - - core.info("Created comment #" + comment.id + ": " + comment.html_url); + let comment; + if (commentEndpoint === "discussions") { + // For discussions, we need to use GraphQL mutation + // First, we need the discussion ID if we don't have it from context + if (!discussionId) { + // Fetch the discussion to get its node_id + const discussionQuery = ` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + discussion(number: $number) { + id + } + } + } + `; + const queryResult = await github.graphql(discussionQuery, { + owner: context.repo.owner, + repo: context.repo.repo, + number: issueNumber, + }); + discussionId = queryResult.repository.discussion.id; + } + + // Create the discussion comment using GraphQL + const addDiscussionCommentMutation = ` + mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: { + discussionId: $discussionId, + body: $body + }) { + comment { + id + url + databaseId + } + } + } + `; + const mutationResult = await github.graphql(addDiscussionCommentMutation, { + discussionId: discussionId, + body: body, + }); + const discussionComment = mutationResult.addDiscussionComment.comment; + // Normalize the response to match REST API structure + comment = { + id: discussionComment.databaseId, + html_url: discussionComment.url, + }; + core.info("Created discussion comment #" + comment.id + ": " + comment.html_url); + } else { + // Create the comment using GitHub REST API for issues/PRs + const { data: restComment } = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: body, + }); + comment = restComment; + core.info("Created comment #" + comment.id + ": " + comment.html_url); + } + createdComments.push(comment); // Set output for the last created comment (for backward compatibility) diff --git a/pkg/workflow/js/safe_outputs_mcp_server.cjs b/pkg/workflow/js/safe_outputs_mcp_server.cjs index 334bccd1ef..6bf46d0f91 100644 --- a/pkg/workflow/js/safe_outputs_mcp_server.cjs +++ b/pkg/workflow/js/safe_outputs_mcp_server.cjs @@ -367,7 +367,7 @@ const ALL_TOOLS = [ }, { name: "add_comment", - description: "Add a comment to a GitHub issue or pull request", + description: "Add a comment to a GitHub issue, pull request, or discussion", inputSchema: { type: "object", required: ["body"], @@ -377,6 +377,10 @@ const ALL_TOOLS = [ type: "number", description: "Issue or PR number (optional for current context)", }, + discussion_number: { + type: "number", + description: "Discussion number (optional for current context)", + }, }, additionalProperties: false, }, diff --git a/pkg/workflow/js/types/safe-outputs.d.ts b/pkg/workflow/js/types/safe-outputs.d.ts index 92b8566bfe..76f81fbc56 100644 --- a/pkg/workflow/js/types/safe-outputs.d.ts +++ b/pkg/workflow/js/types/safe-outputs.d.ts @@ -40,12 +40,16 @@ interface CreateDiscussionItem extends BaseSafeOutputItem { } /** - * JSONL item for adding a comment to an issue or PR + * JSONL item for adding a comment to an issue, PR, or discussion */ interface AddCommentItem extends BaseSafeOutputItem { type: "add-comment"; /** Comment body content */ body: string; + /** Optional issue or PR number when using target: "*" */ + issue_number?: number; + /** Optional discussion number when using target: "*" */ + discussion_number?: number; } /**