diff --git a/.changeset/patch-add-discussion-interaction-smoke-workflows.md b/.changeset/patch-add-discussion-interaction-smoke-workflows.md new file mode 100644 index 0000000000..439a4e0e1e --- /dev/null +++ b/.changeset/patch-add-discussion-interaction-smoke-workflows.md @@ -0,0 +1,13 @@ +--- +"gh-aw": patch +--- + +Add discussion interaction to smoke workflows and serialize the discussion +flag in safe-outputs handler config. + +Smoke workflows now select a random discussion and post thematic comments to +validate discussion comment functionality. The compiler now emits the +`"discussion": true` flag in `GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG` when a +workflow requests discussion output, and lock files include `discussions: write` +permission where applicable. + diff --git a/.changeset/patch-add-discussion-interaction-to-smoke-workflows.md b/.changeset/patch-add-discussion-interaction-to-smoke-workflows.md new file mode 100644 index 0000000000..b601ac5e49 --- /dev/null +++ b/.changeset/patch-add-discussion-interaction-to-smoke-workflows.md @@ -0,0 +1,7 @@ +--- +"gh-aw": patch +--- + +Add discussion interaction to smoke workflows; compiler now serializes the `discussion` flag into the safe-outputs handler config so workflows can post comments to discussions. Lock files include `discussions: write` where applicable. + +Smoke workflows pick a random discussion and post a thematic comment (copilot: playful, claude: comic-book, codex: mystical oracle, opencode: space mission). This is a non-breaking tooling/workflow change. diff --git a/.changeset/patch-add-discussion-smoke-workflows.md b/.changeset/patch-add-discussion-smoke-workflows.md new file mode 100644 index 0000000000..df62950865 --- /dev/null +++ b/.changeset/patch-add-discussion-smoke-workflows.md @@ -0,0 +1,13 @@ +--- +"gh-aw": patch +--- + +Add discussion interaction to smoke workflows; deprecate the `discussion` flag and +add a codemod to remove it. Smoke workflows now query discussions and post +comments to both discussions and PRs to validate discussion functionality. + +The compiler no longer emits a `discussion` boolean flag in compiled handler +configs; the `add_comment` handler auto-detects target type or accepts a +`discussion_number` parameter. A codemod `add-comment-discussion-removal` is +available via `gh aw fix --write` to remove the deprecated field from workflows. + diff --git a/.github/workflows/daily-performance-summary.lock.yml b/.github/workflows/daily-performance-summary.lock.yml index f081540eb4..0d187a71f4 100644 --- a/.github/workflows/daily-performance-summary.lock.yml +++ b/.github/workflows/daily-performance-summary.lock.yml @@ -621,16 +621,76 @@ jobs: LIMIT="${INPUT_LIMIT:-30}" JQ_FILTER="${INPUT_JQ:-}" - # JSON fields to fetch - JSON_FIELDS="number,title,author,createdAt,updatedAt,body,category,labels,comments,answer,url" - - # Build and execute gh command + # Parse repository owner and name if [[ -n "$REPO" ]]; then - OUTPUT=$(gh discussion list --limit "$LIMIT" --json "$JSON_FIELDS" --repo "$REPO") + OWNER=$(echo "$REPO" | cut -d'/' -f1) + NAME=$(echo "$REPO" | cut -d'/' -f2) else - OUTPUT=$(gh discussion list --limit "$LIMIT" --json "$JSON_FIELDS") + # Get current repository from GitHub context + OWNER="${GITHUB_REPOSITORY_OWNER:-}" + NAME=$(echo "${GITHUB_REPOSITORY:-}" | cut -d'/' -f2) + fi + + # Validate owner and name + if [[ -z "$OWNER" || -z "$NAME" ]]; then + echo "Error: Could not determine repository owner and name" >&2 + exit 1 fi + # Build GraphQL query for discussions + GRAPHQL_QUERY=$(cat <&2 + exit 1 + fi + + # Build GraphQL query for discussions + GRAPHQL_QUERY=$(cat <&2 + exit 1 fi + # Build GraphQL query for discussions + GRAPHQL_QUERY=$(cat <&2 + exit 1 + fi + + # Build GraphQL query for discussions + GRAPHQL_QUERY=$(cat < /opt/gh-aw/safeoutputs/config.json << 'EOF' - {"add_comment":{"max":1},"add_labels":{"allowed":["smoke-claude"],"max":3},"create_issue":{"group":true,"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + {"add_comment":{"max":2},"add_labels":{"allowed":["smoke-claude"],"max":3},"create_issue":{"group":true,"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' [ @@ -254,7 +256,7 @@ jobs: "name": "create_issue" }, { - "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 1 comment(s) can be added.", + "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 2 comment(s) can be added.", "inputSchema": { "additionalProperties": false, "properties": { @@ -532,6 +534,92 @@ jobs: "GH_DEBUG": "GH_DEBUG" }, "timeout": 60 + }, + { + "name": "github-discussion-query", + "description": "Query GitHub discussions with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter.", + "inputSchema": { + "properties": { + "jq": { + "description": "jq filter expression to apply to output. If not provided, returns schema info instead of full data.", + "type": "string" + }, + "limit": { + "description": "Maximum number of discussions to fetch (default: 30)", + "type": "number" + }, + "repo": { + "description": "Repository in owner/repo format (defaults to current repository)", + "type": "string" + } + }, + "type": "object" + }, + "handler": "github-discussion-query.sh", + "env": { + "GH_TOKEN": "GH_TOKEN" + }, + "timeout": 60 + }, + { + "name": "github-issue-query", + "description": "Query GitHub issues with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter.", + "inputSchema": { + "properties": { + "jq": { + "description": "jq filter expression to apply to output. If not provided, returns schema info instead of full data.", + "type": "string" + }, + "limit": { + "description": "Maximum number of issues to fetch (default: 30)", + "type": "number" + }, + "repo": { + "description": "Repository in owner/repo format (defaults to current repository)", + "type": "string" + }, + "state": { + "description": "Issue state: open, closed, all (default: open)", + "type": "string" + } + }, + "type": "object" + }, + "handler": "github-issue-query.sh", + "env": { + "GH_TOKEN": "GH_TOKEN" + }, + "timeout": 60 + }, + { + "name": "github-pr-query", + "description": "Query GitHub pull requests with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter.", + "inputSchema": { + "properties": { + "jq": { + "description": "jq filter expression to apply to output. If not provided, returns schema info instead of full data.", + "type": "string" + }, + "limit": { + "description": "Maximum number of PRs to fetch (default: 30)", + "type": "number" + }, + "repo": { + "description": "Repository in owner/repo format (defaults to current repository)", + "type": "string" + }, + "state": { + "description": "PR state: open, closed, merged, all (default: open)", + "type": "string" + } + }, + "type": "object" + }, + "handler": "github-pr-query.sh", + "env": { + "GH_TOKEN": "GH_TOKEN" + }, + "timeout": 60 } ] } @@ -568,6 +656,311 @@ jobs: EOFSH_gh chmod +x /opt/gh-aw/safe-inputs/gh.sh + cat > /opt/gh-aw/safe-inputs/github-discussion-query.sh << 'EOFSH_github-discussion-query' + #!/bin/bash + # Auto-generated safe-input tool: github-discussion-query + # Query GitHub discussions with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. + + set -euo pipefail + + set -e + + # Default values + REPO="${INPUT_REPO:-}" + LIMIT="${INPUT_LIMIT:-30}" + JQ_FILTER="${INPUT_JQ:-}" + + # Parse repository owner and name + if [[ -n "$REPO" ]]; then + OWNER=$(echo "$REPO" | cut -d'/' -f1) + NAME=$(echo "$REPO" | cut -d'/' -f2) + else + # Get current repository from GitHub context + OWNER="${GITHUB_REPOSITORY_OWNER:-}" + NAME=$(echo "${GITHUB_REPOSITORY:-}" | cut -d'/' -f2) + fi + + # Validate owner and name + if [[ -z "$OWNER" || -z "$NAME" ]]; then + echo "Error: Could not determine repository owner and name" >&2 + exit 1 + fi + + # Build GraphQL query for discussions + GRAPHQL_QUERY=$(cat < /opt/gh-aw/safe-inputs/github-issue-query.sh << 'EOFSH_github-issue-query' + #!/bin/bash + # Auto-generated safe-input tool: github-issue-query + # Query GitHub issues with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. + + set -euo pipefail + + set -e + + # Default values + REPO="${INPUT_REPO:-}" + STATE="${INPUT_STATE:-open}" + LIMIT="${INPUT_LIMIT:-30}" + JQ_FILTER="${INPUT_JQ:-}" + + # JSON fields to fetch + JSON_FIELDS="number,title,state,author,createdAt,updatedAt,closedAt,body,labels,assignees,comments,milestone,url" + + # Build and execute gh command + if [[ -n "$REPO" ]]; then + OUTPUT=$(gh issue list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS" --repo "$REPO") + else + OUTPUT=$(gh issue list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS") + fi + + # Apply jq filter if specified + if [[ -n "$JQ_FILTER" ]]; then + jq "$JQ_FILTER" <<< "$OUTPUT" + else + # Return schema and size instead of full data + ITEM_COUNT=$(jq 'length' <<< "$OUTPUT") + DATA_SIZE=${#OUTPUT} + + # Validate values are numeric + if ! [[ "$ITEM_COUNT" =~ ^[0-9]+$ ]]; then + ITEM_COUNT=0 + fi + if ! [[ "$DATA_SIZE" =~ ^[0-9]+$ ]]; then + DATA_SIZE=0 + fi + + cat << EOF + { + "message": "No --jq filter provided. Use --jq to filter and retrieve data.", + "item_count": $ITEM_COUNT, + "data_size_bytes": $DATA_SIZE, + "schema": { + "type": "array", + "description": "Array of issue objects", + "item_fields": { + "number": "integer - Issue number", + "title": "string - Issue title", + "state": "string - Issue state (OPEN, CLOSED)", + "author": "object - Author info with login field", + "createdAt": "string - ISO timestamp of creation", + "updatedAt": "string - ISO timestamp of last update", + "closedAt": "string|null - ISO timestamp of close", + "body": "string - Issue body content", + "labels": "array - Array of label objects with name field", + "assignees": "array - Array of assignee objects with login field", + "comments": "object - Comments info with totalCount field", + "milestone": "object|null - Milestone info with title field", + "url": "string - Issue URL" + } + }, + "suggested_queries": [ + {"description": "Get all data", "query": "."}, + {"description": "Get issue numbers and titles", "query": ".[] | {number, title}"}, + {"description": "Get open issues only", "query": ".[] | select(.state == \"OPEN\")"}, + {"description": "Get issues by author", "query": ".[] | select(.author.login == \"USERNAME\")"}, + {"description": "Get issues with label", "query": ".[] | select(.labels | map(.name) | index(\"bug\"))"}, + {"description": "Get issues with many comments", "query": ".[] | select(.comments.totalCount > 5) | {number, title, comments: .comments.totalCount}"}, + {"description": "Count by state", "query": "group_by(.state) | map({state: .[0].state, count: length})"} + ] + } + EOF + fi + + + EOFSH_github-issue-query + chmod +x /opt/gh-aw/safe-inputs/github-issue-query.sh + cat > /opt/gh-aw/safe-inputs/github-pr-query.sh << 'EOFSH_github-pr-query' + #!/bin/bash + # Auto-generated safe-input tool: github-pr-query + # Query GitHub pull requests with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. + + set -euo pipefail + + set -e + + # Default values + REPO="${INPUT_REPO:-}" + STATE="${INPUT_STATE:-open}" + LIMIT="${INPUT_LIMIT:-30}" + JQ_FILTER="${INPUT_JQ:-}" + + # JSON fields to fetch + JSON_FIELDS="number,title,state,author,createdAt,updatedAt,mergedAt,closedAt,headRefName,baseRefName,isDraft,reviewDecision,additions,deletions,changedFiles,labels,assignees,reviewRequests,url" + + # Build and execute gh command + if [[ -n "$REPO" ]]; then + OUTPUT=$(gh pr list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS" --repo "$REPO") + else + OUTPUT=$(gh pr list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS") + fi + + # Apply jq filter if specified + if [[ -n "$JQ_FILTER" ]]; then + jq "$JQ_FILTER" <<< "$OUTPUT" + else + # Return schema and size instead of full data + ITEM_COUNT=$(jq 'length' <<< "$OUTPUT") + DATA_SIZE=${#OUTPUT} + + # Validate values are numeric + if ! [[ "$ITEM_COUNT" =~ ^[0-9]+$ ]]; then + ITEM_COUNT=0 + fi + if ! [[ "$DATA_SIZE" =~ ^[0-9]+$ ]]; then + DATA_SIZE=0 + fi + + cat << EOF + { + "message": "No --jq filter provided. Use --jq to filter and retrieve data.", + "item_count": $ITEM_COUNT, + "data_size_bytes": $DATA_SIZE, + "schema": { + "type": "array", + "description": "Array of pull request objects", + "item_fields": { + "number": "integer - PR number", + "title": "string - PR title", + "state": "string - PR state (OPEN, CLOSED, MERGED)", + "author": "object - Author info with login field", + "createdAt": "string - ISO timestamp of creation", + "updatedAt": "string - ISO timestamp of last update", + "mergedAt": "string|null - ISO timestamp of merge", + "closedAt": "string|null - ISO timestamp of close", + "headRefName": "string - Source branch name", + "baseRefName": "string - Target branch name", + "isDraft": "boolean - Whether PR is a draft", + "reviewDecision": "string|null - Review decision (APPROVED, CHANGES_REQUESTED, REVIEW_REQUIRED)", + "additions": "integer - Lines added", + "deletions": "integer - Lines deleted", + "changedFiles": "integer - Number of files changed", + "labels": "array - Array of label objects with name field", + "assignees": "array - Array of assignee objects with login field", + "reviewRequests": "array - Array of review request objects", + "url": "string - PR URL" + } + }, + "suggested_queries": [ + {"description": "Get all data", "query": "."}, + {"description": "Get PR numbers and titles", "query": ".[] | {number, title}"}, + {"description": "Get open PRs only", "query": ".[] | select(.state == \"OPEN\")"}, + {"description": "Get merged PRs", "query": ".[] | select(.mergedAt != null)"}, + {"description": "Get PRs by author", "query": ".[] | select(.author.login == \"USERNAME\")"}, + {"description": "Get large PRs", "query": ".[] | select(.changedFiles > 10) | {number, title, changedFiles}"}, + {"description": "Count by state", "query": "group_by(.state) | map({state: .[0].state, count: length})"} + ] + } + EOF + fi + + + EOFSH_github-pr-query + chmod +x /opt/gh-aw/safe-inputs/github-pr-query.sh - name: Generate Safe Inputs MCP Server Config id: safe-inputs-config @@ -595,6 +988,7 @@ jobs: GH_AW_SAFE_INPUTS_API_KEY: ${{ steps.safe-inputs-config.outputs.safe_inputs_api_key }} GH_AW_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_DEBUG: 1 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Environment variables are set above to prevent template injection export GH_AW_SAFE_INPUTS_PORT @@ -612,6 +1006,7 @@ jobs: GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} GH_DEBUG: 1 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} @@ -629,7 +1024,7 @@ jobs: # Register API key as secret to mask it from logs echo "::add-mask::${MCP_GATEWAY_API_KEY}" export GH_AW_ENGINE="claude" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e DEBUG="*" -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_INPUTS_PORT -e GH_AW_SAFE_INPUTS_API_KEY -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GH_AW_GH_TOKEN -e GH_DEBUG -e TAVILY_API_KEY -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/githubnext/gh-aw-mcpg:v0.0.76' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e DEBUG="*" -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_INPUTS_PORT -e GH_AW_SAFE_INPUTS_API_KEY -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GH_AW_GH_TOKEN -e GH_DEBUG -e GH_TOKEN -e TAVILY_API_KEY -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/githubnext/gh-aw-mcpg:v0.0.76' cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh { @@ -1044,6 +1439,8 @@ jobs: - Include up to 3 most relevant run URLs at end under `**References:**` - Do NOT add footer attribution (system adds automatically) + + # Smoke Test: Claude Engine Validation **IMPORTANT: Keep all outputs extremely short and concise. Use single-line responses where possible. No verbose explanations.** @@ -1057,6 +1454,10 @@ jobs: 5. **Tavily Web Search Testing**: Use the Tavily MCP server to perform a web search for "GitHub Agentic Workflows" and verify that results are returned with at least one item 6. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-claude-__GH_AW_GITHUB_RUN_ID__.txt` with content "Smoke test passed for Claude at $(date)" (create the directory if it doesn't exist) 7. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) + 8. **Discussion Interaction Testing**: + - Use the `github-discussion-query` safe-input tool with params: `limit=1, jq=".[0]"` to get the latest discussion from __GH_AW_GITHUB_REPOSITORY__ + - Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123) + - Use the `add_comment` tool with `discussion_number: ` to add a fun, comic-book style comment stating that the smoke test agent was here ## Output @@ -1073,6 +1474,8 @@ jobs: - ✅ or ❌ for each test result - Overall status: PASS or FAIL + 3. Use the `add_comment` tool to add a **fun comic-book style comment** to the latest discussion (using the `discussion_number` you extracted in step 8) - be playful and use comic-book language like "💥 WHOOSH!" + If all tests pass, add the label `smoke-claude` to the pull request. PROMPT_EOF @@ -1737,7 +2140,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"smoke-claude\"]},\"create_issue\":{\"close_older_issues\":true,\"expires\":2,\"group\":true,\"max\":1},\"missing_data\":{},\"missing_tool\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-claude\"]},\"create_issue\":{\"close_older_issues\":true,\"expires\":2,\"group\":true,\"max\":1},\"missing_data\":{},\"missing_tool\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-claude.md b/.github/workflows/smoke-claude.md index 6fe2aa7980..a3240b64c3 100644 --- a/.github/workflows/smoke-claude.md +++ b/.github/workflows/smoke-claude.md @@ -11,6 +11,7 @@ permissions: contents: read issues: read pull-requests: read + discussions: read name: Smoke Claude engine: @@ -22,6 +23,7 @@ imports: - shared/gh.md - shared/mcp/tavily.md - shared/reporting.md + - shared/github-queries-safe-input.md network: allowed: - defaults @@ -49,6 +51,7 @@ runtimes: safe-outputs: add-comment: hide-older-comments: true + max: 2 create-issue: expires: 2h group: true @@ -76,6 +79,10 @@ timeout-minutes: 10 5. **Tavily Web Search Testing**: Use the Tavily MCP server to perform a web search for "GitHub Agentic Workflows" and verify that results are returned with at least one item 6. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-claude-${{ github.run_id }}.txt` with content "Smoke test passed for Claude at $(date)" (create the directory if it doesn't exist) 7. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) +8. **Discussion Interaction Testing**: + - Use the `github-discussion-query` safe-input tool with params: `limit=1, jq=".[0]"` to get the latest discussion from ${{ github.repository }} + - Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123) + - Use the `add_comment` tool with `discussion_number: ` to add a fun, comic-book style comment stating that the smoke test agent was here ## Output @@ -92,4 +99,6 @@ timeout-minutes: 10 - ✅ or ❌ for each test result - Overall status: PASS or FAIL +3. Use the `add_comment` tool to add a **fun comic-book style comment** to the latest discussion (using the `discussion_number` you extracted in step 8) - be playful and use comic-book language like "💥 WHOOSH!" + If all tests pass, add the label `smoke-claude` to the pull request. diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index a3c8f2f16e..471ad46291 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -24,6 +24,7 @@ # Resolved workflow manifest: # Imports: # - shared/gh.md +# - shared/github-queries-safe-input.md # - shared/mcp/tavily.md # - shared/reporting.md @@ -102,6 +103,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + discussions: read issues: read pull-requests: read env: @@ -208,7 +210,7 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' - {"add_comment":{"max":1},"add_labels":{"allowed":["smoke-codex"],"max":3},"create_issue":{"max":1},"hide_comment":{"max":5},"missing_data":{},"missing_tool":{},"noop":{"max":1},"remove_labels":{"allowed":["smoke"],"max":3}} + {"add_comment":{"max":2},"add_labels":{"allowed":["smoke-codex"],"max":3},"create_issue":{"max":1},"hide_comment":{"max":5},"missing_data":{},"missing_tool":{},"noop":{"max":1},"remove_labels":{"allowed":["smoke"],"max":3}} EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' [ @@ -253,7 +255,7 @@ jobs: "name": "create_issue" }, { - "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 1 comment(s) can be added.", + "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 2 comment(s) can be added.", "inputSchema": { "additionalProperties": false, "properties": { @@ -559,6 +561,92 @@ jobs: "GH_DEBUG": "GH_DEBUG" }, "timeout": 60 + }, + { + "name": "github-discussion-query", + "description": "Query GitHub discussions with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter.", + "inputSchema": { + "properties": { + "jq": { + "description": "jq filter expression to apply to output. If not provided, returns schema info instead of full data.", + "type": "string" + }, + "limit": { + "description": "Maximum number of discussions to fetch (default: 30)", + "type": "number" + }, + "repo": { + "description": "Repository in owner/repo format (defaults to current repository)", + "type": "string" + } + }, + "type": "object" + }, + "handler": "github-discussion-query.sh", + "env": { + "GH_TOKEN": "GH_TOKEN" + }, + "timeout": 60 + }, + { + "name": "github-issue-query", + "description": "Query GitHub issues with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter.", + "inputSchema": { + "properties": { + "jq": { + "description": "jq filter expression to apply to output. If not provided, returns schema info instead of full data.", + "type": "string" + }, + "limit": { + "description": "Maximum number of issues to fetch (default: 30)", + "type": "number" + }, + "repo": { + "description": "Repository in owner/repo format (defaults to current repository)", + "type": "string" + }, + "state": { + "description": "Issue state: open, closed, all (default: open)", + "type": "string" + } + }, + "type": "object" + }, + "handler": "github-issue-query.sh", + "env": { + "GH_TOKEN": "GH_TOKEN" + }, + "timeout": 60 + }, + { + "name": "github-pr-query", + "description": "Query GitHub pull requests with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter.", + "inputSchema": { + "properties": { + "jq": { + "description": "jq filter expression to apply to output. If not provided, returns schema info instead of full data.", + "type": "string" + }, + "limit": { + "description": "Maximum number of PRs to fetch (default: 30)", + "type": "number" + }, + "repo": { + "description": "Repository in owner/repo format (defaults to current repository)", + "type": "string" + }, + "state": { + "description": "PR state: open, closed, merged, all (default: open)", + "type": "string" + } + }, + "type": "object" + }, + "handler": "github-pr-query.sh", + "env": { + "GH_TOKEN": "GH_TOKEN" + }, + "timeout": 60 } ] } @@ -595,6 +683,311 @@ jobs: EOFSH_gh chmod +x /opt/gh-aw/safe-inputs/gh.sh + cat > /opt/gh-aw/safe-inputs/github-discussion-query.sh << 'EOFSH_github-discussion-query' + #!/bin/bash + # Auto-generated safe-input tool: github-discussion-query + # Query GitHub discussions with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. + + set -euo pipefail + + set -e + + # Default values + REPO="${INPUT_REPO:-}" + LIMIT="${INPUT_LIMIT:-30}" + JQ_FILTER="${INPUT_JQ:-}" + + # Parse repository owner and name + if [[ -n "$REPO" ]]; then + OWNER=$(echo "$REPO" | cut -d'/' -f1) + NAME=$(echo "$REPO" | cut -d'/' -f2) + else + # Get current repository from GitHub context + OWNER="${GITHUB_REPOSITORY_OWNER:-}" + NAME=$(echo "${GITHUB_REPOSITORY:-}" | cut -d'/' -f2) + fi + + # Validate owner and name + if [[ -z "$OWNER" || -z "$NAME" ]]; then + echo "Error: Could not determine repository owner and name" >&2 + exit 1 + fi + + # Build GraphQL query for discussions + GRAPHQL_QUERY=$(cat < /opt/gh-aw/safe-inputs/github-issue-query.sh << 'EOFSH_github-issue-query' + #!/bin/bash + # Auto-generated safe-input tool: github-issue-query + # Query GitHub issues with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. + + set -euo pipefail + + set -e + + # Default values + REPO="${INPUT_REPO:-}" + STATE="${INPUT_STATE:-open}" + LIMIT="${INPUT_LIMIT:-30}" + JQ_FILTER="${INPUT_JQ:-}" + + # JSON fields to fetch + JSON_FIELDS="number,title,state,author,createdAt,updatedAt,closedAt,body,labels,assignees,comments,milestone,url" + + # Build and execute gh command + if [[ -n "$REPO" ]]; then + OUTPUT=$(gh issue list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS" --repo "$REPO") + else + OUTPUT=$(gh issue list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS") + fi + + # Apply jq filter if specified + if [[ -n "$JQ_FILTER" ]]; then + jq "$JQ_FILTER" <<< "$OUTPUT" + else + # Return schema and size instead of full data + ITEM_COUNT=$(jq 'length' <<< "$OUTPUT") + DATA_SIZE=${#OUTPUT} + + # Validate values are numeric + if ! [[ "$ITEM_COUNT" =~ ^[0-9]+$ ]]; then + ITEM_COUNT=0 + fi + if ! [[ "$DATA_SIZE" =~ ^[0-9]+$ ]]; then + DATA_SIZE=0 + fi + + cat << EOF + { + "message": "No --jq filter provided. Use --jq to filter and retrieve data.", + "item_count": $ITEM_COUNT, + "data_size_bytes": $DATA_SIZE, + "schema": { + "type": "array", + "description": "Array of issue objects", + "item_fields": { + "number": "integer - Issue number", + "title": "string - Issue title", + "state": "string - Issue state (OPEN, CLOSED)", + "author": "object - Author info with login field", + "createdAt": "string - ISO timestamp of creation", + "updatedAt": "string - ISO timestamp of last update", + "closedAt": "string|null - ISO timestamp of close", + "body": "string - Issue body content", + "labels": "array - Array of label objects with name field", + "assignees": "array - Array of assignee objects with login field", + "comments": "object - Comments info with totalCount field", + "milestone": "object|null - Milestone info with title field", + "url": "string - Issue URL" + } + }, + "suggested_queries": [ + {"description": "Get all data", "query": "."}, + {"description": "Get issue numbers and titles", "query": ".[] | {number, title}"}, + {"description": "Get open issues only", "query": ".[] | select(.state == \"OPEN\")"}, + {"description": "Get issues by author", "query": ".[] | select(.author.login == \"USERNAME\")"}, + {"description": "Get issues with label", "query": ".[] | select(.labels | map(.name) | index(\"bug\"))"}, + {"description": "Get issues with many comments", "query": ".[] | select(.comments.totalCount > 5) | {number, title, comments: .comments.totalCount}"}, + {"description": "Count by state", "query": "group_by(.state) | map({state: .[0].state, count: length})"} + ] + } + EOF + fi + + + EOFSH_github-issue-query + chmod +x /opt/gh-aw/safe-inputs/github-issue-query.sh + cat > /opt/gh-aw/safe-inputs/github-pr-query.sh << 'EOFSH_github-pr-query' + #!/bin/bash + # Auto-generated safe-input tool: github-pr-query + # Query GitHub pull requests with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. + + set -euo pipefail + + set -e + + # Default values + REPO="${INPUT_REPO:-}" + STATE="${INPUT_STATE:-open}" + LIMIT="${INPUT_LIMIT:-30}" + JQ_FILTER="${INPUT_JQ:-}" + + # JSON fields to fetch + JSON_FIELDS="number,title,state,author,createdAt,updatedAt,mergedAt,closedAt,headRefName,baseRefName,isDraft,reviewDecision,additions,deletions,changedFiles,labels,assignees,reviewRequests,url" + + # Build and execute gh command + if [[ -n "$REPO" ]]; then + OUTPUT=$(gh pr list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS" --repo "$REPO") + else + OUTPUT=$(gh pr list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS") + fi + + # Apply jq filter if specified + if [[ -n "$JQ_FILTER" ]]; then + jq "$JQ_FILTER" <<< "$OUTPUT" + else + # Return schema and size instead of full data + ITEM_COUNT=$(jq 'length' <<< "$OUTPUT") + DATA_SIZE=${#OUTPUT} + + # Validate values are numeric + if ! [[ "$ITEM_COUNT" =~ ^[0-9]+$ ]]; then + ITEM_COUNT=0 + fi + if ! [[ "$DATA_SIZE" =~ ^[0-9]+$ ]]; then + DATA_SIZE=0 + fi + + cat << EOF + { + "message": "No --jq filter provided. Use --jq to filter and retrieve data.", + "item_count": $ITEM_COUNT, + "data_size_bytes": $DATA_SIZE, + "schema": { + "type": "array", + "description": "Array of pull request objects", + "item_fields": { + "number": "integer - PR number", + "title": "string - PR title", + "state": "string - PR state (OPEN, CLOSED, MERGED)", + "author": "object - Author info with login field", + "createdAt": "string - ISO timestamp of creation", + "updatedAt": "string - ISO timestamp of last update", + "mergedAt": "string|null - ISO timestamp of merge", + "closedAt": "string|null - ISO timestamp of close", + "headRefName": "string - Source branch name", + "baseRefName": "string - Target branch name", + "isDraft": "boolean - Whether PR is a draft", + "reviewDecision": "string|null - Review decision (APPROVED, CHANGES_REQUESTED, REVIEW_REQUIRED)", + "additions": "integer - Lines added", + "deletions": "integer - Lines deleted", + "changedFiles": "integer - Number of files changed", + "labels": "array - Array of label objects with name field", + "assignees": "array - Array of assignee objects with login field", + "reviewRequests": "array - Array of review request objects", + "url": "string - PR URL" + } + }, + "suggested_queries": [ + {"description": "Get all data", "query": "."}, + {"description": "Get PR numbers and titles", "query": ".[] | {number, title}"}, + {"description": "Get open PRs only", "query": ".[] | select(.state == \"OPEN\")"}, + {"description": "Get merged PRs", "query": ".[] | select(.mergedAt != null)"}, + {"description": "Get PRs by author", "query": ".[] | select(.author.login == \"USERNAME\")"}, + {"description": "Get large PRs", "query": ".[] | select(.changedFiles > 10) | {number, title, changedFiles}"}, + {"description": "Count by state", "query": "group_by(.state) | map({state: .[0].state, count: length})"} + ] + } + EOF + fi + + + EOFSH_github-pr-query + chmod +x /opt/gh-aw/safe-inputs/github-pr-query.sh - name: Generate Safe Inputs MCP Server Config id: safe-inputs-config @@ -622,6 +1015,7 @@ jobs: GH_AW_SAFE_INPUTS_API_KEY: ${{ steps.safe-inputs-config.outputs.safe_inputs_api_key }} GH_AW_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_DEBUG: 1 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Environment variables are set above to prevent template injection export GH_AW_SAFE_INPUTS_PORT @@ -639,6 +1033,7 @@ jobs: GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} GH_DEBUG: 1 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} @@ -656,7 +1051,7 @@ jobs: # Register API key as secret to mask it from logs echo "::add-mask::${MCP_GATEWAY_API_KEY}" export GH_AW_ENGINE="codex" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e DEBUG="*" -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_INPUTS_PORT -e GH_AW_SAFE_INPUTS_API_KEY -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GH_AW_GH_TOKEN -e GH_DEBUG -e TAVILY_API_KEY -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/githubnext/gh-aw-mcpg:v0.0.76' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e DEBUG="*" -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_INPUTS_PORT -e GH_AW_SAFE_INPUTS_API_KEY -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GH_AW_GH_TOKEN -e GH_DEBUG -e GH_TOKEN -e TAVILY_API_KEY -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/githubnext/gh-aw-mcpg:v0.0.76' cat > /tmp/gh-aw/mcp-config/config.toml << EOF [history] @@ -1026,6 +1421,8 @@ jobs: - Include up to 3 most relevant run URLs at end under `**References:**` - Do NOT add footer attribution (system adds automatically) + + # Smoke Test: Codex Engine Validation **IMPORTANT: Keep all outputs extremely short and concise. Use single-line responses where possible. No verbose explanations.** @@ -1039,6 +1436,10 @@ jobs: 5. **Tavily Web Search Testing**: Use the Tavily MCP server to perform a web search for "GitHub Agentic Workflows" and verify that results are returned with at least one item 6. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-codex-__GH_AW_GITHUB_RUN_ID__.txt` with content "Smoke test passed for Codex at $(date)" (create the directory if it doesn't exist) 7. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) + 8. **Discussion Interaction Testing**: + - Use the `github-discussion-query` safe-input tool with params: `limit=1, jq=".[0]"` to get the latest discussion from __GH_AW_GITHUB_REPOSITORY__ + - Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123) + - Use the `add_comment` tool with `discussion_number: ` to add a mystical, oracle-themed comment stating that the smoke test agent was here ## Output @@ -1047,6 +1448,8 @@ jobs: - ✅ or ❌ for each test result - Overall status: PASS or FAIL + Use the `add_comment` tool to add a **mystical oracle-themed comment** to the latest discussion (using the `discussion_number` you extracted in step 8) - be creative and use mystical language like "🔮 The ancient spirits stir..." + If all tests pass, add the label `smoke-codex` to the pull request. PROMPT_EOF @@ -1592,7 +1995,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"smoke-codex\"]},\"create_issue\":{\"close_older_issues\":true,\"expires\":2,\"max\":1},\"hide_comment\":{\"max\":5},\"missing_data\":{},\"missing_tool\":{},\"remove_labels\":{\"allowed\":[\"smoke\"]}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-codex\"]},\"create_issue\":{\"close_older_issues\":true,\"expires\":2,\"max\":1},\"hide_comment\":{\"max\":5},\"missing_data\":{},\"missing_tool\":{},\"remove_labels\":{\"allowed\":[\"smoke\"]}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-codex.md b/.github/workflows/smoke-codex.md index b6c21ab088..c98f9518f1 100644 --- a/.github/workflows/smoke-codex.md +++ b/.github/workflows/smoke-codex.md @@ -11,6 +11,7 @@ permissions: contents: read issues: read pull-requests: read + discussions: read name: Smoke Codex engine: codex strict: true @@ -18,6 +19,7 @@ imports: - shared/gh.md - shared/mcp/tavily.md - shared/reporting.md + - shared/github-queries-safe-input.md network: allowed: - defaults @@ -44,6 +46,7 @@ sandbox: safe-outputs: add-comment: hide-older-comments: true + max: 2 create-issue: expires: 2h close-older-issues: true @@ -73,6 +76,10 @@ timeout-minutes: 10 5. **Tavily Web Search Testing**: Use the Tavily MCP server to perform a web search for "GitHub Agentic Workflows" and verify that results are returned with at least one item 6. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-codex-${{ github.run_id }}.txt` with content "Smoke test passed for Codex at $(date)" (create the directory if it doesn't exist) 7. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) +8. **Discussion Interaction Testing**: + - Use the `github-discussion-query` safe-input tool with params: `limit=1, jq=".[0]"` to get the latest discussion from ${{ github.repository }} + - Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123) + - Use the `add_comment` tool with `discussion_number: ` to add a mystical, oracle-themed comment stating that the smoke test agent was here ## Output @@ -81,4 +88,6 @@ Add a **very brief** comment (max 5-10 lines) to the current pull request with: - ✅ or ❌ for each test result - Overall status: PASS or FAIL +Use the `add_comment` tool to add a **mystical oracle-themed comment** to the latest discussion (using the `discussion_number` you extracted in step 8) - be creative and use mystical language like "🔮 The ancient spirits stir..." + If all tests pass, add the label `smoke-codex` to the pull request. diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 69356f38d0..e990314dc6 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -24,6 +24,7 @@ # Resolved workflow manifest: # Imports: # - shared/gh.md +# - shared/github-queries-safe-input.md # - shared/reporting.md name: "Smoke Copilot" @@ -102,6 +103,7 @@ jobs: permissions: actions: read contents: read + discussions: read issues: read pull-requests: read env: @@ -226,7 +228,7 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' - {"add_comment":{"max":1},"add_labels":{"allowed":["smoke-copilot"],"max":3},"create_issue":{"group":true,"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1},"remove_labels":{"allowed":["smoke"],"max":3}} + {"add_comment":{"max":2},"add_labels":{"allowed":["smoke-copilot"],"max":3},"create_issue":{"group":true,"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1},"remove_labels":{"allowed":["smoke"],"max":3}} EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' [ @@ -271,7 +273,7 @@ jobs: "name": "create_issue" }, { - "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 1 comment(s) can be added.", + "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 2 comment(s) can be added.", "inputSchema": { "additionalProperties": false, "properties": { @@ -549,6 +551,92 @@ jobs: "GH_DEBUG": "GH_DEBUG" }, "timeout": 60 + }, + { + "name": "github-discussion-query", + "description": "Query GitHub discussions with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter.", + "inputSchema": { + "properties": { + "jq": { + "description": "jq filter expression to apply to output. If not provided, returns schema info instead of full data.", + "type": "string" + }, + "limit": { + "description": "Maximum number of discussions to fetch (default: 30)", + "type": "number" + }, + "repo": { + "description": "Repository in owner/repo format (defaults to current repository)", + "type": "string" + } + }, + "type": "object" + }, + "handler": "github-discussion-query.sh", + "env": { + "GH_TOKEN": "GH_TOKEN" + }, + "timeout": 60 + }, + { + "name": "github-issue-query", + "description": "Query GitHub issues with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter.", + "inputSchema": { + "properties": { + "jq": { + "description": "jq filter expression to apply to output. If not provided, returns schema info instead of full data.", + "type": "string" + }, + "limit": { + "description": "Maximum number of issues to fetch (default: 30)", + "type": "number" + }, + "repo": { + "description": "Repository in owner/repo format (defaults to current repository)", + "type": "string" + }, + "state": { + "description": "Issue state: open, closed, all (default: open)", + "type": "string" + } + }, + "type": "object" + }, + "handler": "github-issue-query.sh", + "env": { + "GH_TOKEN": "GH_TOKEN" + }, + "timeout": 60 + }, + { + "name": "github-pr-query", + "description": "Query GitHub pull requests with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter.", + "inputSchema": { + "properties": { + "jq": { + "description": "jq filter expression to apply to output. If not provided, returns schema info instead of full data.", + "type": "string" + }, + "limit": { + "description": "Maximum number of PRs to fetch (default: 30)", + "type": "number" + }, + "repo": { + "description": "Repository in owner/repo format (defaults to current repository)", + "type": "string" + }, + "state": { + "description": "PR state: open, closed, merged, all (default: open)", + "type": "string" + } + }, + "type": "object" + }, + "handler": "github-pr-query.sh", + "env": { + "GH_TOKEN": "GH_TOKEN" + }, + "timeout": 60 } ] } @@ -585,6 +673,311 @@ jobs: EOFSH_gh chmod +x /opt/gh-aw/safe-inputs/gh.sh + cat > /opt/gh-aw/safe-inputs/github-discussion-query.sh << 'EOFSH_github-discussion-query' + #!/bin/bash + # Auto-generated safe-input tool: github-discussion-query + # Query GitHub discussions with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. + + set -euo pipefail + + set -e + + # Default values + REPO="${INPUT_REPO:-}" + LIMIT="${INPUT_LIMIT:-30}" + JQ_FILTER="${INPUT_JQ:-}" + + # Parse repository owner and name + if [[ -n "$REPO" ]]; then + OWNER=$(echo "$REPO" | cut -d'/' -f1) + NAME=$(echo "$REPO" | cut -d'/' -f2) + else + # Get current repository from GitHub context + OWNER="${GITHUB_REPOSITORY_OWNER:-}" + NAME=$(echo "${GITHUB_REPOSITORY:-}" | cut -d'/' -f2) + fi + + # Validate owner and name + if [[ -z "$OWNER" || -z "$NAME" ]]; then + echo "Error: Could not determine repository owner and name" >&2 + exit 1 + fi + + # Build GraphQL query for discussions + GRAPHQL_QUERY=$(cat < /opt/gh-aw/safe-inputs/github-issue-query.sh << 'EOFSH_github-issue-query' + #!/bin/bash + # Auto-generated safe-input tool: github-issue-query + # Query GitHub issues with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. + + set -euo pipefail + + set -e + + # Default values + REPO="${INPUT_REPO:-}" + STATE="${INPUT_STATE:-open}" + LIMIT="${INPUT_LIMIT:-30}" + JQ_FILTER="${INPUT_JQ:-}" + + # JSON fields to fetch + JSON_FIELDS="number,title,state,author,createdAt,updatedAt,closedAt,body,labels,assignees,comments,milestone,url" + + # Build and execute gh command + if [[ -n "$REPO" ]]; then + OUTPUT=$(gh issue list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS" --repo "$REPO") + else + OUTPUT=$(gh issue list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS") + fi + + # Apply jq filter if specified + if [[ -n "$JQ_FILTER" ]]; then + jq "$JQ_FILTER" <<< "$OUTPUT" + else + # Return schema and size instead of full data + ITEM_COUNT=$(jq 'length' <<< "$OUTPUT") + DATA_SIZE=${#OUTPUT} + + # Validate values are numeric + if ! [[ "$ITEM_COUNT" =~ ^[0-9]+$ ]]; then + ITEM_COUNT=0 + fi + if ! [[ "$DATA_SIZE" =~ ^[0-9]+$ ]]; then + DATA_SIZE=0 + fi + + cat << EOF + { + "message": "No --jq filter provided. Use --jq to filter and retrieve data.", + "item_count": $ITEM_COUNT, + "data_size_bytes": $DATA_SIZE, + "schema": { + "type": "array", + "description": "Array of issue objects", + "item_fields": { + "number": "integer - Issue number", + "title": "string - Issue title", + "state": "string - Issue state (OPEN, CLOSED)", + "author": "object - Author info with login field", + "createdAt": "string - ISO timestamp of creation", + "updatedAt": "string - ISO timestamp of last update", + "closedAt": "string|null - ISO timestamp of close", + "body": "string - Issue body content", + "labels": "array - Array of label objects with name field", + "assignees": "array - Array of assignee objects with login field", + "comments": "object - Comments info with totalCount field", + "milestone": "object|null - Milestone info with title field", + "url": "string - Issue URL" + } + }, + "suggested_queries": [ + {"description": "Get all data", "query": "."}, + {"description": "Get issue numbers and titles", "query": ".[] | {number, title}"}, + {"description": "Get open issues only", "query": ".[] | select(.state == \"OPEN\")"}, + {"description": "Get issues by author", "query": ".[] | select(.author.login == \"USERNAME\")"}, + {"description": "Get issues with label", "query": ".[] | select(.labels | map(.name) | index(\"bug\"))"}, + {"description": "Get issues with many comments", "query": ".[] | select(.comments.totalCount > 5) | {number, title, comments: .comments.totalCount}"}, + {"description": "Count by state", "query": "group_by(.state) | map({state: .[0].state, count: length})"} + ] + } + EOF + fi + + + EOFSH_github-issue-query + chmod +x /opt/gh-aw/safe-inputs/github-issue-query.sh + cat > /opt/gh-aw/safe-inputs/github-pr-query.sh << 'EOFSH_github-pr-query' + #!/bin/bash + # Auto-generated safe-input tool: github-pr-query + # Query GitHub pull requests with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. + + set -euo pipefail + + set -e + + # Default values + REPO="${INPUT_REPO:-}" + STATE="${INPUT_STATE:-open}" + LIMIT="${INPUT_LIMIT:-30}" + JQ_FILTER="${INPUT_JQ:-}" + + # JSON fields to fetch + JSON_FIELDS="number,title,state,author,createdAt,updatedAt,mergedAt,closedAt,headRefName,baseRefName,isDraft,reviewDecision,additions,deletions,changedFiles,labels,assignees,reviewRequests,url" + + # Build and execute gh command + if [[ -n "$REPO" ]]; then + OUTPUT=$(gh pr list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS" --repo "$REPO") + else + OUTPUT=$(gh pr list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS") + fi + + # Apply jq filter if specified + if [[ -n "$JQ_FILTER" ]]; then + jq "$JQ_FILTER" <<< "$OUTPUT" + else + # Return schema and size instead of full data + ITEM_COUNT=$(jq 'length' <<< "$OUTPUT") + DATA_SIZE=${#OUTPUT} + + # Validate values are numeric + if ! [[ "$ITEM_COUNT" =~ ^[0-9]+$ ]]; then + ITEM_COUNT=0 + fi + if ! [[ "$DATA_SIZE" =~ ^[0-9]+$ ]]; then + DATA_SIZE=0 + fi + + cat << EOF + { + "message": "No --jq filter provided. Use --jq to filter and retrieve data.", + "item_count": $ITEM_COUNT, + "data_size_bytes": $DATA_SIZE, + "schema": { + "type": "array", + "description": "Array of pull request objects", + "item_fields": { + "number": "integer - PR number", + "title": "string - PR title", + "state": "string - PR state (OPEN, CLOSED, MERGED)", + "author": "object - Author info with login field", + "createdAt": "string - ISO timestamp of creation", + "updatedAt": "string - ISO timestamp of last update", + "mergedAt": "string|null - ISO timestamp of merge", + "closedAt": "string|null - ISO timestamp of close", + "headRefName": "string - Source branch name", + "baseRefName": "string - Target branch name", + "isDraft": "boolean - Whether PR is a draft", + "reviewDecision": "string|null - Review decision (APPROVED, CHANGES_REQUESTED, REVIEW_REQUIRED)", + "additions": "integer - Lines added", + "deletions": "integer - Lines deleted", + "changedFiles": "integer - Number of files changed", + "labels": "array - Array of label objects with name field", + "assignees": "array - Array of assignee objects with login field", + "reviewRequests": "array - Array of review request objects", + "url": "string - PR URL" + } + }, + "suggested_queries": [ + {"description": "Get all data", "query": "."}, + {"description": "Get PR numbers and titles", "query": ".[] | {number, title}"}, + {"description": "Get open PRs only", "query": ".[] | select(.state == \"OPEN\")"}, + {"description": "Get merged PRs", "query": ".[] | select(.mergedAt != null)"}, + {"description": "Get PRs by author", "query": ".[] | select(.author.login == \"USERNAME\")"}, + {"description": "Get large PRs", "query": ".[] | select(.changedFiles > 10) | {number, title, changedFiles}"}, + {"description": "Count by state", "query": "group_by(.state) | map({state: .[0].state, count: length})"} + ] + } + EOF + fi + + + EOFSH_github-pr-query + chmod +x /opt/gh-aw/safe-inputs/github-pr-query.sh - name: Generate Safe Inputs MCP Server Config id: safe-inputs-config @@ -612,6 +1005,7 @@ jobs: GH_AW_SAFE_INPUTS_API_KEY: ${{ steps.safe-inputs-config.outputs.safe_inputs_api_key }} GH_AW_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_DEBUG: 1 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Environment variables are set above to prevent template injection export GH_AW_SAFE_INPUTS_PORT @@ -629,6 +1023,7 @@ jobs: GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} GH_DEBUG: 1 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -646,7 +1041,7 @@ jobs: # Register API key as secret to mask it from logs echo "::add-mask::${MCP_GATEWAY_API_KEY}" export GH_AW_ENGINE="copilot" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e DEBUG="*" -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_INPUTS_PORT -e GH_AW_SAFE_INPUTS_API_KEY -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GH_AW_GH_TOKEN -e GH_DEBUG -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/githubnext/gh-aw-mcpg:v0.0.76' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e DEBUG="*" -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_INPUTS_PORT -e GH_AW_SAFE_INPUTS_API_KEY -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GH_AW_GH_TOKEN -e GH_DEBUG -e GH_TOKEN -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/githubnext/gh-aw-mcpg:v0.0.76' mkdir -p /home/runner/.copilot cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh @@ -935,6 +1330,8 @@ jobs: - Include up to 3 most relevant run URLs at end under `**References:**` - Do NOT add footer attribution (system adds automatically) + + # Smoke Test: Copilot Engine Validation **IMPORTANT: Keep all outputs extremely short and concise. Use single-line responses where possible. No verbose explanations.** @@ -947,6 +1344,10 @@ jobs: 4. **Playwright Testing**: Use playwright to navigate to and verify the page title contains "GitHub" 5. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-copilot-__GH_AW_GITHUB_RUN_ID__.txt` with content "Smoke test passed for Copilot at $(date)" (create the directory if it doesn't exist) 6. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) + 7. **Discussion Interaction Testing**: + - Use the `github-discussion-query` safe-input tool with params: `limit=1, jq=".[0]"` to get the latest discussion from __GH_AW_GITHUB_REPOSITORY__ + - Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123) + - Use the `add_comment` tool with `discussion_number: ` to add a fun, playful comment stating that the smoke test agent was here ## Output @@ -965,6 +1366,8 @@ jobs: - Overall status: PASS or FAIL - Mention the pull request author and any assignees + 3. Use the `add_comment` tool to add a **fun and creative comment** to the latest discussion (using the `discussion_number` you extracted in step 7) - be playful and entertaining in your comment + If all tests pass: - Add the label `smoke-copilot` to the pull request - Remove the label `smoke` from the pull request @@ -1541,7 +1944,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"smoke-copilot\"]},\"create_issue\":{\"close_older_issues\":true,\"expires\":2,\"group\":true,\"max\":1},\"missing_data\":{},\"missing_tool\":{},\"remove_labels\":{\"allowed\":[\"smoke\"]}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-copilot\"]},\"create_issue\":{\"close_older_issues\":true,\"expires\":2,\"group\":true,\"max\":1},\"missing_data\":{},\"missing_tool\":{},\"remove_labels\":{\"allowed\":[\"smoke\"]}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index 07a2d35b82..a8e913aaae 100644 --- a/.github/workflows/smoke-copilot.md +++ b/.github/workflows/smoke-copilot.md @@ -11,12 +11,14 @@ permissions: contents: read pull-requests: read issues: read + discussions: read actions: read name: Smoke Copilot engine: copilot imports: - shared/gh.md - shared/reporting.md + - shared/github-queries-safe-input.md network: allowed: - defaults @@ -46,6 +48,7 @@ sandbox: safe-outputs: add-comment: hide-older-comments: true + max: 2 create-issue: expires: 2h group: true @@ -76,6 +79,10 @@ strict: true 4. **Playwright Testing**: Use playwright to navigate to and verify the page title contains "GitHub" 5. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-copilot-${{ github.run_id }}.txt` with content "Smoke test passed for Copilot at $(date)" (create the directory if it doesn't exist) 6. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) +7. **Discussion Interaction Testing**: + - Use the `github-discussion-query` safe-input tool with params: `limit=1, jq=".[0]"` to get the latest discussion from ${{ github.repository }} + - Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123) + - Use the `add_comment` tool with `discussion_number: ` to add a fun, playful comment stating that the smoke test agent was here ## Output @@ -94,6 +101,8 @@ strict: true - Overall status: PASS or FAIL - Mention the pull request author and any assignees +3. Use the `add_comment` tool to add a **fun and creative comment** to the latest discussion (using the `discussion_number` you extracted in step 7) - be playful and entertaining in your comment + If all tests pass: - Add the label `smoke-copilot` to the pull request - Remove the label `smoke` from the pull request diff --git a/.github/workflows/smoke-opencode.lock.yml b/.github/workflows/smoke-opencode.lock.yml index c7484d24f7..3318176c08 100644 --- a/.github/workflows/smoke-opencode.lock.yml +++ b/.github/workflows/smoke-opencode.lock.yml @@ -24,6 +24,7 @@ # Resolved workflow manifest: # Imports: # - shared/gh.md +# - shared/github-queries-safe-input.md # - shared/opencode.md name: "Smoke OpenCode" @@ -101,6 +102,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + discussions: read issues: read pull-requests: read env: @@ -182,7 +184,7 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > /opt/gh-aw/safeoutputs/config.json << 'EOF' - {"add_comment":{"max":1},"add_labels":{"allowed":["smoke-opencode"],"max":3},"create_issue":{"group":true,"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + {"add_comment":{"max":2},"add_labels":{"allowed":["smoke-opencode"],"max":3},"create_issue":{"group":true,"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' [ @@ -227,7 +229,7 @@ jobs: "name": "create_issue" }, { - "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 1 comment(s) can be added.", + "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. CONSTRAINTS: Maximum 2 comment(s) can be added.", "inputSchema": { "additionalProperties": false, "properties": { @@ -505,6 +507,92 @@ jobs: "GH_DEBUG": "GH_DEBUG" }, "timeout": 60 + }, + { + "name": "github-discussion-query", + "description": "Query GitHub discussions with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter.", + "inputSchema": { + "properties": { + "jq": { + "description": "jq filter expression to apply to output. If not provided, returns schema info instead of full data.", + "type": "string" + }, + "limit": { + "description": "Maximum number of discussions to fetch (default: 30)", + "type": "number" + }, + "repo": { + "description": "Repository in owner/repo format (defaults to current repository)", + "type": "string" + } + }, + "type": "object" + }, + "handler": "github-discussion-query.sh", + "env": { + "GH_TOKEN": "GH_TOKEN" + }, + "timeout": 60 + }, + { + "name": "github-issue-query", + "description": "Query GitHub issues with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter.", + "inputSchema": { + "properties": { + "jq": { + "description": "jq filter expression to apply to output. If not provided, returns schema info instead of full data.", + "type": "string" + }, + "limit": { + "description": "Maximum number of issues to fetch (default: 30)", + "type": "number" + }, + "repo": { + "description": "Repository in owner/repo format (defaults to current repository)", + "type": "string" + }, + "state": { + "description": "Issue state: open, closed, all (default: open)", + "type": "string" + } + }, + "type": "object" + }, + "handler": "github-issue-query.sh", + "env": { + "GH_TOKEN": "GH_TOKEN" + }, + "timeout": 60 + }, + { + "name": "github-pr-query", + "description": "Query GitHub pull requests with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter.", + "inputSchema": { + "properties": { + "jq": { + "description": "jq filter expression to apply to output. If not provided, returns schema info instead of full data.", + "type": "string" + }, + "limit": { + "description": "Maximum number of PRs to fetch (default: 30)", + "type": "number" + }, + "repo": { + "description": "Repository in owner/repo format (defaults to current repository)", + "type": "string" + }, + "state": { + "description": "PR state: open, closed, merged, all (default: open)", + "type": "string" + } + }, + "type": "object" + }, + "handler": "github-pr-query.sh", + "env": { + "GH_TOKEN": "GH_TOKEN" + }, + "timeout": 60 } ] } @@ -541,6 +629,311 @@ jobs: EOFSH_gh chmod +x /opt/gh-aw/safe-inputs/gh.sh + cat > /opt/gh-aw/safe-inputs/github-discussion-query.sh << 'EOFSH_github-discussion-query' + #!/bin/bash + # Auto-generated safe-input tool: github-discussion-query + # Query GitHub discussions with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. + + set -euo pipefail + + set -e + + # Default values + REPO="${INPUT_REPO:-}" + LIMIT="${INPUT_LIMIT:-30}" + JQ_FILTER="${INPUT_JQ:-}" + + # Parse repository owner and name + if [[ -n "$REPO" ]]; then + OWNER=$(echo "$REPO" | cut -d'/' -f1) + NAME=$(echo "$REPO" | cut -d'/' -f2) + else + # Get current repository from GitHub context + OWNER="${GITHUB_REPOSITORY_OWNER:-}" + NAME=$(echo "${GITHUB_REPOSITORY:-}" | cut -d'/' -f2) + fi + + # Validate owner and name + if [[ -z "$OWNER" || -z "$NAME" ]]; then + echo "Error: Could not determine repository owner and name" >&2 + exit 1 + fi + + # Build GraphQL query for discussions + GRAPHQL_QUERY=$(cat < /opt/gh-aw/safe-inputs/github-issue-query.sh << 'EOFSH_github-issue-query' + #!/bin/bash + # Auto-generated safe-input tool: github-issue-query + # Query GitHub issues with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. + + set -euo pipefail + + set -e + + # Default values + REPO="${INPUT_REPO:-}" + STATE="${INPUT_STATE:-open}" + LIMIT="${INPUT_LIMIT:-30}" + JQ_FILTER="${INPUT_JQ:-}" + + # JSON fields to fetch + JSON_FIELDS="number,title,state,author,createdAt,updatedAt,closedAt,body,labels,assignees,comments,milestone,url" + + # Build and execute gh command + if [[ -n "$REPO" ]]; then + OUTPUT=$(gh issue list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS" --repo "$REPO") + else + OUTPUT=$(gh issue list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS") + fi + + # Apply jq filter if specified + if [[ -n "$JQ_FILTER" ]]; then + jq "$JQ_FILTER" <<< "$OUTPUT" + else + # Return schema and size instead of full data + ITEM_COUNT=$(jq 'length' <<< "$OUTPUT") + DATA_SIZE=${#OUTPUT} + + # Validate values are numeric + if ! [[ "$ITEM_COUNT" =~ ^[0-9]+$ ]]; then + ITEM_COUNT=0 + fi + if ! [[ "$DATA_SIZE" =~ ^[0-9]+$ ]]; then + DATA_SIZE=0 + fi + + cat << EOF + { + "message": "No --jq filter provided. Use --jq to filter and retrieve data.", + "item_count": $ITEM_COUNT, + "data_size_bytes": $DATA_SIZE, + "schema": { + "type": "array", + "description": "Array of issue objects", + "item_fields": { + "number": "integer - Issue number", + "title": "string - Issue title", + "state": "string - Issue state (OPEN, CLOSED)", + "author": "object - Author info with login field", + "createdAt": "string - ISO timestamp of creation", + "updatedAt": "string - ISO timestamp of last update", + "closedAt": "string|null - ISO timestamp of close", + "body": "string - Issue body content", + "labels": "array - Array of label objects with name field", + "assignees": "array - Array of assignee objects with login field", + "comments": "object - Comments info with totalCount field", + "milestone": "object|null - Milestone info with title field", + "url": "string - Issue URL" + } + }, + "suggested_queries": [ + {"description": "Get all data", "query": "."}, + {"description": "Get issue numbers and titles", "query": ".[] | {number, title}"}, + {"description": "Get open issues only", "query": ".[] | select(.state == \"OPEN\")"}, + {"description": "Get issues by author", "query": ".[] | select(.author.login == \"USERNAME\")"}, + {"description": "Get issues with label", "query": ".[] | select(.labels | map(.name) | index(\"bug\"))"}, + {"description": "Get issues with many comments", "query": ".[] | select(.comments.totalCount > 5) | {number, title, comments: .comments.totalCount}"}, + {"description": "Count by state", "query": "group_by(.state) | map({state: .[0].state, count: length})"} + ] + } + EOF + fi + + + EOFSH_github-issue-query + chmod +x /opt/gh-aw/safe-inputs/github-issue-query.sh + cat > /opt/gh-aw/safe-inputs/github-pr-query.sh << 'EOFSH_github-pr-query' + #!/bin/bash + # Auto-generated safe-input tool: github-pr-query + # Query GitHub pull requests with jq filtering support. Without --jq, returns schema and data size info. Use --jq '.' to get all data, or specific jq expressions to filter. + + set -euo pipefail + + set -e + + # Default values + REPO="${INPUT_REPO:-}" + STATE="${INPUT_STATE:-open}" + LIMIT="${INPUT_LIMIT:-30}" + JQ_FILTER="${INPUT_JQ:-}" + + # JSON fields to fetch + JSON_FIELDS="number,title,state,author,createdAt,updatedAt,mergedAt,closedAt,headRefName,baseRefName,isDraft,reviewDecision,additions,deletions,changedFiles,labels,assignees,reviewRequests,url" + + # Build and execute gh command + if [[ -n "$REPO" ]]; then + OUTPUT=$(gh pr list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS" --repo "$REPO") + else + OUTPUT=$(gh pr list --state "$STATE" --limit "$LIMIT" --json "$JSON_FIELDS") + fi + + # Apply jq filter if specified + if [[ -n "$JQ_FILTER" ]]; then + jq "$JQ_FILTER" <<< "$OUTPUT" + else + # Return schema and size instead of full data + ITEM_COUNT=$(jq 'length' <<< "$OUTPUT") + DATA_SIZE=${#OUTPUT} + + # Validate values are numeric + if ! [[ "$ITEM_COUNT" =~ ^[0-9]+$ ]]; then + ITEM_COUNT=0 + fi + if ! [[ "$DATA_SIZE" =~ ^[0-9]+$ ]]; then + DATA_SIZE=0 + fi + + cat << EOF + { + "message": "No --jq filter provided. Use --jq to filter and retrieve data.", + "item_count": $ITEM_COUNT, + "data_size_bytes": $DATA_SIZE, + "schema": { + "type": "array", + "description": "Array of pull request objects", + "item_fields": { + "number": "integer - PR number", + "title": "string - PR title", + "state": "string - PR state (OPEN, CLOSED, MERGED)", + "author": "object - Author info with login field", + "createdAt": "string - ISO timestamp of creation", + "updatedAt": "string - ISO timestamp of last update", + "mergedAt": "string|null - ISO timestamp of merge", + "closedAt": "string|null - ISO timestamp of close", + "headRefName": "string - Source branch name", + "baseRefName": "string - Target branch name", + "isDraft": "boolean - Whether PR is a draft", + "reviewDecision": "string|null - Review decision (APPROVED, CHANGES_REQUESTED, REVIEW_REQUIRED)", + "additions": "integer - Lines added", + "deletions": "integer - Lines deleted", + "changedFiles": "integer - Number of files changed", + "labels": "array - Array of label objects with name field", + "assignees": "array - Array of assignee objects with login field", + "reviewRequests": "array - Array of review request objects", + "url": "string - PR URL" + } + }, + "suggested_queries": [ + {"description": "Get all data", "query": "."}, + {"description": "Get PR numbers and titles", "query": ".[] | {number, title}"}, + {"description": "Get open PRs only", "query": ".[] | select(.state == \"OPEN\")"}, + {"description": "Get merged PRs", "query": ".[] | select(.mergedAt != null)"}, + {"description": "Get PRs by author", "query": ".[] | select(.author.login == \"USERNAME\")"}, + {"description": "Get large PRs", "query": ".[] | select(.changedFiles > 10) | {number, title, changedFiles}"}, + {"description": "Count by state", "query": "group_by(.state) | map({state: .[0].state, count: length})"} + ] + } + EOF + fi + + + EOFSH_github-pr-query + chmod +x /opt/gh-aw/safe-inputs/github-pr-query.sh - name: Generate Safe Inputs MCP Server Config id: safe-inputs-config @@ -568,6 +961,7 @@ jobs: GH_AW_SAFE_INPUTS_API_KEY: ${{ steps.safe-inputs-config.outputs.safe_inputs_api_key }} GH_AW_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_DEBUG: 1 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Environment variables are set above to prevent template injection export GH_AW_SAFE_INPUTS_PORT @@ -585,6 +979,7 @@ jobs: GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} GH_DEBUG: 1 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | @@ -601,7 +996,7 @@ jobs: # Register API key as secret to mask it from logs echo "::add-mask::${MCP_GATEWAY_API_KEY}" export GH_AW_ENGINE="custom" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e DEBUG="*" -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_INPUTS_PORT -e GH_AW_SAFE_INPUTS_API_KEY -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GH_AW_GH_TOKEN -e GH_DEBUG -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/githubnext/gh-aw-mcpg:v0.0.76' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e DEBUG="*" -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_INPUTS_PORT -e GH_AW_SAFE_INPUTS_API_KEY -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GH_AW_GH_TOKEN -e GH_DEBUG -e GH_TOKEN -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/githubnext/gh-aw-mcpg:v0.0.76' cat << MCPCONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh { @@ -774,6 +1169,8 @@ jobs: + + # Smoke Test: OpenCode Custom Engine Validation **IMPORTANT: Keep all outputs extremely short and concise. Use single-line responses where possible. No verbose explanations.** @@ -786,6 +1183,10 @@ jobs: 4. **Playwright Testing**: Use playwright to navigate to https://github.com and verify the page title contains "GitHub" 5. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-opencode-__GH_AW_GITHUB_RUN_ID__.txt` with content "Smoke test passed for OpenCode at $(date)" (create the directory if it doesn't exist) 6. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) + 7. **Discussion Interaction Testing**: + - Use the `github-discussion-query` safe-input tool with params: `limit=1, jq=".[0]"` to get the latest discussion from __GH_AW_GITHUB_REPOSITORY__ + - Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123) + - Use the `add_comment` tool with `discussion_number: ` to add a space/rocket-themed comment stating that the smoke test agent was here ## Output @@ -802,6 +1203,8 @@ jobs: - ✅ or ❌ for each test result - Overall status: PASS or FAIL + 3. Use the `add_comment` tool to add a **space/rocket-themed comment** to the latest discussion (using the `discussion_number` you extracted in step 7) - be creative and use space mission language like "🚀 IGNITION!" + If all tests pass, add the label `smoke-opencode` to the pull request. PROMPT_EOF @@ -1443,7 +1846,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"smoke-opencode\"]},\"create_issue\":{\"close_older_issues\":true,\"expires\":2,\"group\":true,\"max\":1},\"missing_data\":{},\"missing_tool\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":2},\"add_labels\":{\"allowed\":[\"smoke-opencode\"]},\"create_issue\":{\"close_older_issues\":true,\"expires\":2,\"group\":true,\"max\":1},\"missing_data\":{},\"missing_tool\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/smoke-opencode.md b/.github/workflows/smoke-opencode.md index f24ee5508f..fa4fc0f9ed 100644 --- a/.github/workflows/smoke-opencode.md +++ b/.github/workflows/smoke-opencode.md @@ -11,11 +11,13 @@ permissions: contents: read issues: read pull-requests: read + discussions: read name: Smoke OpenCode imports: - shared/opencode.md - shared/gh.md + - shared/github-queries-safe-input.md strict: true sandbox: mcp: @@ -36,6 +38,7 @@ tools: safe-outputs: add-comment: hide-older-comments: true + max: 2 create-issue: expires: 2h group: true @@ -62,6 +65,10 @@ timeout-minutes: 10 4. **Playwright Testing**: Use playwright to navigate to https://github.com and verify the page title contains "GitHub" 5. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-opencode-${{ github.run_id }}.txt` with content "Smoke test passed for OpenCode at $(date)" (create the directory if it doesn't exist) 6. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back) +7. **Discussion Interaction Testing**: + - Use the `github-discussion-query` safe-input tool with params: `limit=1, jq=".[0]"` to get the latest discussion from ${{ github.repository }} + - Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123) + - Use the `add_comment` tool with `discussion_number: ` to add a space/rocket-themed comment stating that the smoke test agent was here ## Output @@ -78,4 +85,6 @@ timeout-minutes: 10 - ✅ or ❌ for each test result - Overall status: PASS or FAIL +3. Use the `add_comment` tool to add a **space/rocket-themed comment** to the latest discussion (using the `discussion_number` you extracted in step 7) - be creative and use space mission language like "🚀 IGNITION!" + If all tests pass, add the label `smoke-opencode` to the pull request. diff --git a/pkg/cli/codemod_discussion_flag.go b/pkg/cli/codemod_discussion_flag.go new file mode 100644 index 0000000000..bcec260650 --- /dev/null +++ b/pkg/cli/codemod_discussion_flag.go @@ -0,0 +1,136 @@ +package cli + +import ( + "strings" + + "github.com/githubnext/gh-aw/pkg/logger" +) + +var discussionFlagCodemodLog = logger.New("cli:codemod_discussion_flag") + +// getDiscussionFlagRemovalCodemod creates a codemod for removing the deprecated discussion field from add-comment +func getDiscussionFlagRemovalCodemod() Codemod { + return Codemod{ + ID: "add-comment-discussion-removal", + Name: "Remove deprecated add-comment.discussion field", + Description: "Removes the deprecated 'safe-outputs.add-comment.discussion' field (detection is now automatic based on context)", + IntroducedIn: "0.3.0", + Apply: func(content string, frontmatter map[string]any) (string, bool, error) { + // Check if safe-outputs exists + safeOutputsValue, hasSafeOutputs := frontmatter["safe-outputs"] + if !hasSafeOutputs { + return content, false, nil + } + + safeOutputsMap, ok := safeOutputsValue.(map[string]any) + if !ok { + return content, false, nil + } + + // Check if add-comment exists in safe-outputs + addCommentValue, hasAddComment := safeOutputsMap["add-comment"] + if !hasAddComment { + return content, false, nil + } + + addCommentMap, ok := addCommentValue.(map[string]any) + if !ok { + return content, false, nil + } + + // Check if discussion field exists in add-comment + _, hasDiscussion := addCommentMap["discussion"] + if !hasDiscussion { + return content, false, nil + } + + // Parse frontmatter to get raw lines + frontmatterLines, markdown, err := parseFrontmatterLines(content) + if err != nil { + return content, false, err + } + + // Remove the discussion field from the add-comment block in safe-outputs + var result []string + var modified bool + var inSafeOutputsBlock bool + var safeOutputsIndent string + var inAddCommentBlock bool + var addCommentIndent string + var inDiscussionField bool + + for i, line := range frontmatterLines { + trimmedLine := strings.TrimSpace(line) + + // Track if we're in the safe-outputs block + if strings.HasPrefix(trimmedLine, "safe-outputs:") { + inSafeOutputsBlock = true + safeOutputsIndent = getIndentation(line) + result = append(result, line) + continue + } + + // Check if we've left the safe-outputs block + if inSafeOutputsBlock && len(trimmedLine) > 0 && !strings.HasPrefix(trimmedLine, "#") { + if hasExitedBlock(line, safeOutputsIndent) { + inSafeOutputsBlock = false + inAddCommentBlock = false + } + } + + // Track if we're in the add-comment block within safe-outputs + if inSafeOutputsBlock && strings.HasPrefix(trimmedLine, "add-comment:") { + inAddCommentBlock = true + addCommentIndent = getIndentation(line) + result = append(result, line) + continue + } + + // Check if we've left the add-comment block + if inAddCommentBlock && len(trimmedLine) > 0 && !strings.HasPrefix(trimmedLine, "#") { + if hasExitedBlock(line, addCommentIndent) { + inAddCommentBlock = false + } + } + + // Remove discussion field line if in add-comment block + if inAddCommentBlock && strings.HasPrefix(trimmedLine, "discussion:") { + modified = true + inDiscussionField = true + discussionFlagCodemodLog.Printf("Removed safe-outputs.add-comment.discussion on line %d", i+1) + continue + } + + // Skip any nested content under the discussion field (shouldn't be any, but for completeness) + if inDiscussionField { + // Empty lines within the field block should be removed + if len(trimmedLine) == 0 { + continue + } + + currentIndent := getIndentation(line) + discussionIndent := addCommentIndent + " " // discussion would be 2 spaces more than add-comment + + // If this line has more indentation than discussion field, skip it + if len(currentIndent) > len(discussionIndent) { + discussionFlagCodemodLog.Printf("Removed nested discussion property on line %d: %s", i+1, trimmedLine) + continue + } + // We've exited the discussion field + inDiscussionField = false + } + + result = append(result, line) + } + + if !modified { + return content, false, nil + } + + // Reconstruct the content + newContent := reconstructContent(result, markdown) + discussionFlagCodemodLog.Print("Applied add-comment.discussion removal") + return newContent, true, nil + }, + } +} diff --git a/pkg/cli/codemod_discussion_flag_test.go b/pkg/cli/codemod_discussion_flag_test.go new file mode 100644 index 0000000000..cc481e8e09 --- /dev/null +++ b/pkg/cli/codemod_discussion_flag_test.go @@ -0,0 +1,294 @@ +package cli + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetDiscussionFlagRemovalCodemod(t *testing.T) { + codemod := getDiscussionFlagRemovalCodemod() + + assert.Equal(t, "add-comment-discussion-removal", codemod.ID) + assert.Equal(t, "Remove deprecated add-comment.discussion field", codemod.Name) + assert.NotEmpty(t, codemod.Description) + assert.Equal(t, "0.3.0", codemod.IntroducedIn) + require.NotNil(t, codemod.Apply) +} + +func TestDiscussionFlagCodemod_RemovesDiscussionFlag(t *testing.T) { + codemod := getDiscussionFlagRemovalCodemod() + + content := `--- +on: workflow_dispatch +safe-outputs: + add-comment: + hide-older-comments: true + discussion: true + max: 2 + create-issue: + expires: 2h +--- + +# Test` + + frontmatter := map[string]any{ + "on": "workflow_dispatch", + "safe-outputs": map[string]any{ + "add-comment": map[string]any{ + "hide-older-comments": true, + "discussion": true, + "max": 2, + }, + "create-issue": map[string]any{ + "expires": "2h", + }, + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + + require.NoError(t, err) + assert.True(t, applied) + assert.NotContains(t, result, "discussion:") + assert.Contains(t, result, "hide-older-comments: true") + assert.Contains(t, result, "max: 2") + assert.Contains(t, result, "expires: 2h") +} + +func TestDiscussionFlagCodemod_NoSafeOutputsField(t *testing.T) { + codemod := getDiscussionFlagRemovalCodemod() + + content := `--- +on: workflow_dispatch +permissions: + contents: read +--- + +# Test` + + frontmatter := map[string]any{ + "on": "workflow_dispatch", + "permissions": map[string]any{ + "contents": "read", + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + + require.NoError(t, err) + assert.False(t, applied) + assert.Equal(t, content, result) +} + +func TestDiscussionFlagCodemod_NoAddCommentField(t *testing.T) { + codemod := getDiscussionFlagRemovalCodemod() + + content := `--- +on: workflow_dispatch +safe-outputs: + create-issue: + expires: 2h +--- + +# Test` + + frontmatter := map[string]any{ + "on": "workflow_dispatch", + "safe-outputs": map[string]any{ + "create-issue": map[string]any{ + "expires": "2h", + }, + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + + require.NoError(t, err) + assert.False(t, applied) + assert.Equal(t, content, result) +} + +func TestDiscussionFlagCodemod_NoDiscussionField(t *testing.T) { + codemod := getDiscussionFlagRemovalCodemod() + + content := `--- +on: workflow_dispatch +safe-outputs: + add-comment: + hide-older-comments: true + max: 2 +--- + +# Test` + + frontmatter := map[string]any{ + "on": "workflow_dispatch", + "safe-outputs": map[string]any{ + "add-comment": map[string]any{ + "hide-older-comments": true, + "max": 2, + }, + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + + require.NoError(t, err) + assert.False(t, applied) + assert.Equal(t, content, result) +} + +func TestDiscussionFlagCodemod_PreservesIndentation(t *testing.T) { + codemod := getDiscussionFlagRemovalCodemod() + + content := `--- +on: workflow_dispatch +safe-outputs: + add-comment: + hide-older-comments: true + discussion: true + max: 2 + create-issue: + expires: 2h +--- + +# Test` + + frontmatter := map[string]any{ + "on": "workflow_dispatch", + "safe-outputs": map[string]any{ + "add-comment": map[string]any{ + "hide-older-comments": true, + "discussion": true, + "max": 2, + }, + "create-issue": map[string]any{ + "expires": "2h", + }, + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + + require.NoError(t, err) + assert.True(t, applied) + assert.NotContains(t, result, "discussion:") + assert.Contains(t, result, " add-comment:") + assert.Contains(t, result, " hide-older-comments: true") + assert.Contains(t, result, " max: 2") +} + +func TestDiscussionFlagCodemod_PreservesComments(t *testing.T) { + codemod := getDiscussionFlagRemovalCodemod() + + content := `--- +on: workflow_dispatch +safe-outputs: + add-comment: + hide-older-comments: true # Hide older comments + discussion: true # Target discussions + max: 2 # Maximum comments +--- + +# Test` + + frontmatter := map[string]any{ + "on": "workflow_dispatch", + "safe-outputs": map[string]any{ + "add-comment": map[string]any{ + "hide-older-comments": true, + "discussion": true, + "max": 2, + }, + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + + require.NoError(t, err) + assert.True(t, applied) + assert.NotContains(t, result, "discussion:") + assert.Contains(t, result, "hide-older-comments: true # Hide older comments") + assert.Contains(t, result, "max: 2 # Maximum comments") +} + +func TestDiscussionFlagCodemod_PreservesMarkdown(t *testing.T) { + codemod := getDiscussionFlagRemovalCodemod() + + content := `--- +on: workflow_dispatch +safe-outputs: + add-comment: + discussion: true +--- + +# Test Workflow + +This workflow uses add-comment with discussion support.` + + frontmatter := map[string]any{ + "on": "workflow_dispatch", + "safe-outputs": map[string]any{ + "add-comment": map[string]any{ + "discussion": true, + }, + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + + require.NoError(t, err) + assert.True(t, applied) + assert.Contains(t, result, "# Test Workflow") + assert.Contains(t, result, "This workflow uses add-comment with discussion support.") +} + +func TestDiscussionFlagCodemod_MultipleFields(t *testing.T) { + codemod := getDiscussionFlagRemovalCodemod() + + content := `--- +on: workflow_dispatch +safe-outputs: + add-comment: + max: 1 + target: "*" + discussion: true + hide-older-comments: false + target-repo: "owner/repo" + create-discussion: + expires: 24h +--- + +# Test` + + frontmatter := map[string]any{ + "on": "workflow_dispatch", + "safe-outputs": map[string]any{ + "add-comment": map[string]any{ + "max": 1, + "target": "*", + "discussion": true, + "hide-older-comments": false, + "target-repo": "owner/repo", + }, + "create-discussion": map[string]any{ + "expires": "24h", + }, + }, + } + + result, applied, err := codemod.Apply(content, frontmatter) + + require.NoError(t, err) + assert.True(t, applied) + // Check that "discussion: true" is not present (but "create-discussion:" is still there) + assert.NotContains(t, result, " discussion: true") + assert.Contains(t, result, "max: 1") + assert.Contains(t, result, "target: \"*\"") + assert.Contains(t, result, "hide-older-comments: false") + assert.Contains(t, result, "target-repo: \"owner/repo\"") + assert.Contains(t, result, "create-discussion:") + assert.Contains(t, result, "expires: 24h") +} diff --git a/pkg/cli/fix_codemods.go b/pkg/cli/fix_codemods.go index 2872fe80ad..71218a20cd 100644 --- a/pkg/cli/fix_codemods.go +++ b/pkg/cli/fix_codemods.go @@ -31,5 +31,6 @@ func GetAllCodemods() []Codemod { getDeleteSchemaFileCodemod(), getGrepToolRemovalCodemod(), getMCPNetworkMigrationCodemod(), + getDiscussionFlagRemovalCodemod(), } } diff --git a/pkg/cli/fix_codemods_test.go b/pkg/cli/fix_codemods_test.go index 8ce5e0642e..2c8301e0ff 100644 --- a/pkg/cli/fix_codemods_test.go +++ b/pkg/cli/fix_codemods_test.go @@ -41,7 +41,7 @@ func TestGetAllCodemods_ReturnsAllCodemods(t *testing.T) { codemods := GetAllCodemods() // Verify we have the expected number of codemods - expectedCount := 13 + expectedCount := 14 assert.Len(t, codemods, expectedCount, "Should return all %d codemods", expectedCount) // Verify all codemods have required fields @@ -115,6 +115,7 @@ func TestGetAllCodemods_InExpectedOrder(t *testing.T) { "delete-schema-file", "grep-tool-removal", "mcp-network-to-top-level-migration", + "add-comment-discussion-removal", } require.Len(t, codemods, len(expectedOrder), "Should have expected number of codemods") diff --git a/pkg/cli/templates/upgrade-agentic-workflows.md b/pkg/cli/templates/upgrade-agentic-workflows.md index b05cb67671..b278e47797 100644 --- a/pkg/cli/templates/upgrade-agentic-workflows.md +++ b/pkg/cli/templates/upgrade-agentic-workflows.md @@ -23,15 +23,16 @@ Read the ENTIRE content of this file carefully before proceeding. Follow the ins - `compile` → compile all workflows - `compile ` → compile a specific workflow -:::note[Command Execution] -When running in GitHub Copilot Cloud, you don't have direct access to `gh aw` CLI commands. Instead, use the **agentic-workflows** MCP tool: -- `fix` tool → apply automatic codemods to fix deprecated fields -- `compile` tool → compile workflows - -When running in other environments with `gh aw` CLI access, prefix commands with `gh aw` (e.g., `gh aw compile`). - -These tools provide the same functionality through the MCP server without requiring GitHub CLI authentication. -::: +> [!NOTE] +> **Command Execution** +> +> When running in GitHub Copilot Cloud, you don't have direct access to `gh aw` CLI commands. Instead, use the **agentic-workflows** MCP tool: +> - `fix` tool → apply automatic codemods to fix deprecated fields +> - `compile` tool → compile workflows +> +> When running in other environments with `gh aw` CLI access, prefix commands with `gh aw` (e.g., `gh aw compile`). +> +> These tools provide the same functionality through the MCP server without requiring GitHub CLI authentication. ## Instructions diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index cac7448c38..3ed640e89c 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -4491,7 +4491,8 @@ "discussion": { "type": "boolean", "const": true, - "description": "Target discussion comments instead of issue/PR comments. Must be true if present." + "description": "DEPRECATED: This field is deprecated and will be removed in a future version. The add_comment handler now automatically detects whether to target discussions based on context (discussion/discussion_comment events) or the item_number field provided by the agent. Remove this field from your workflow configuration.", + "deprecated": true }, "hide-older-comments": { "type": "boolean", diff --git a/pkg/workflow/compiler_safe_outputs_config.go b/pkg/workflow/compiler_safe_outputs_config.go index e492c21f77..e5ecce541c 100644 --- a/pkg/workflow/compiler_safe_outputs_config.go +++ b/pkg/workflow/compiler_safe_outputs_config.go @@ -81,6 +81,8 @@ func (c *Compiler) addHandlerManagerConfigEnvVar(steps *[]string, data *Workflow if len(cfg.AllowedRepos) > 0 { handlerConfig["allowed_repos"] = cfg.AllowedRepos } + // Note: discussion flag is deprecated and not emitted to config + // Discussion support is always available in add_comment handler config["add_comment"] = handlerConfig }