diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 3899211fd6..1927f88a67 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -27,7 +27,7 @@ # - shared/github-queries-safe-input.md # - shared/reporting.md # -# frontmatter-hash: 0899786e057960d0c71d8f7c7d02b43629245f56f3bdbdfcddabc8342163b04a +# frontmatter-hash: ee118bf78a23055f1a6affc1bd1455b1a15e4dd143a9d78b394f931ade7ef0f9 name: "Smoke Copilot" "on": @@ -230,7 +230,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":2},"add_labels":{"allowed":["smoke-copilot"],"max":3},"create_issue":{"expires":2,"group":true,"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1},"remove_labels":{"allowed":["smoke"],"max":3},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"default":null,"description":"The message to send","required":true,"type":"string"}},"output":"Slack message stub executed!"}} + {"add_comment":{"max":2},"add_labels":{"allowed":["smoke-copilot"],"max":3},"create_issue":{"expires":2,"group":true,"max":1},"create_project_status_update":{"max":5},"missing_data":{},"missing_tool":{},"noop":{"max":1},"remove_labels":{"allowed":["smoke"],"max":3},"send-slack-message":{"description":"Send a message to Slack (stub for testing)","inputs":{"message":{"default":null,"description":"The message to send","required":true,"type":"string"}},"output":"Slack message stub executed!"},"update_project":{"max":20}} EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'EOF' [ @@ -382,6 +382,133 @@ jobs: }, "name": "noop" }, + { + "description": "Add or update items in GitHub Projects v2 boards. Can add issues/PRs to a project and update custom field values. Requires the project URL, content type (issue or pull_request), and content number. Use campaign_id to group related items.\n\nThree usage modes:\n1. Add/update project item: Requires project + content_type. For 'issue' or 'pull_request', also requires content_number. For 'draft_issue', requires draft_title.\n2. Create project fields: Requires project + operation='create_fields' + field_definitions.\n3. Create project view: Requires project + operation='create_view' + view.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "campaign_id": { + "description": "Campaign identifier to group related project items. Used to track items created by the same campaign or workflow run.", + "type": "string" + }, + "content_number": { + "description": "Issue or pull request number to add to the project. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123 for issue #123, or 456 in github.com/owner/repo/pull/456 for PR #456). Required when content_type is 'issue' or 'pull_request'.", + "type": "number" + }, + "content_type": { + "description": "Type of item to add to the project. Use 'issue' or 'pull_request' to add existing repo content, or 'draft_issue' to create a draft item inside the project. Required when operation is not specified.", + "enum": [ + "issue", + "pull_request", + "draft_issue" + ], + "type": "string" + }, + "create_if_missing": { + "description": "Whether to create the project if it doesn't exist. Defaults to false. Requires projects:write permission when true.", + "type": "boolean" + }, + "draft_body": { + "description": "Optional body for a Projects v2 draft issue (markdown). Only used when content_type is 'draft_issue'.", + "type": "string" + }, + "draft_title": { + "description": "Title for a Projects v2 draft issue. Required when content_type is 'draft_issue'.", + "type": "string" + }, + "field_definitions": { + "description": "Field definitions to create when operation is create_fields. Required when operation='create_fields'.", + "items": { + "additionalProperties": false, + "properties": { + "data_type": { + "description": "Field type. Use SINGLE_SELECT with options for enumerated values.", + "enum": [ + "TEXT", + "NUMBER", + "DATE", + "SINGLE_SELECT", + "ITERATION" + ], + "type": "string" + }, + "name": { + "description": "Field name to create (e.g., 'size', 'priority').", + "type": "string" + }, + "options": { + "description": "Options for SINGLE_SELECT fields.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "name", + "data_type" + ], + "type": "object" + }, + "type": "array" + }, + "fields": { + "description": "Custom field values to set on the project item (e.g., {'Status': 'In Progress', 'Priority': 'High'}). Field names must match custom fields defined in the project.", + "type": "object" + }, + "operation": { + "description": "Optional operation mode. Use create_fields to create required campaign fields up-front, or create_view to add a project view. When omitted, the tool adds/updates project items.", + "enum": [ + "create_fields", + "create_view" + ], + "type": "string" + }, + "project": { + "description": "Full GitHub project URL (e.g., 'https://github.com/orgs/myorg/projects/42' or 'https://github.com/users/username/projects/5'). Project names or numbers alone are NOT accepted.", + "pattern": "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+$", + "type": "string" + }, + "view": { + "additionalProperties": false, + "description": "View definition to create when operation is create_view. Required when operation='create_view'.", + "properties": { + "filter": { + "type": "string" + }, + "layout": { + "enum": [ + "table", + "board", + "roadmap" + ], + "type": "string" + }, + "name": { + "type": "string" + }, + "visible_fields": { + "description": "Field IDs to show in the view (table/board only).", + "items": { + "type": "number" + }, + "type": "array" + } + }, + "required": [ + "name", + "layout" + ], + "type": "object" + } + }, + "required": [ + "project" + ], + "type": "object" + }, + "name": "update_project" + }, { "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", "inputSchema": { @@ -409,6 +536,50 @@ jobs: }, "name": "missing_data" }, + { + "description": "Create a status update on a GitHub Projects v2 board to communicate project progress. Use this when you need to provide stakeholder updates with status indicators, timeline information, and progress summaries. Status updates create a historical record of project progress tracked over time. Requires project URL, status indicator, dates, and markdown body describing progress/trends/findings.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "Status update body in markdown format describing progress, findings, trends, and next steps. Should provide stakeholders with clear understanding of project state.", + "type": "string" + }, + "project": { + "description": "Full GitHub project URL (e.g., 'https://github.com/orgs/myorg/projects/42' or 'https://github.com/users/username/projects/5'). Project names or numbers alone are NOT accepted.", + "pattern": "^https://github\\\\.com/(orgs|users)/[^/]+/projects/\\\\d+$", + "type": "string" + }, + "start_date": { + "description": "Optional project start date in YYYY-MM-DD format (e.g., '2026-01-06').", + "pattern": "^\\\\d{4}-\\\\d{2}-\\\\d{2}$", + "type": "string" + }, + "status": { + "description": "Status indicator for the project. Defaults to ON_TRACK. Values: ON_TRACK (progressing well), AT_RISK (has issues/blockers), OFF_TRACK (significantly behind), COMPLETE (finished), INACTIVE (paused/cancelled).", + "enum": [ + "ON_TRACK", + "AT_RISK", + "OFF_TRACK", + "COMPLETE", + "INACTIVE" + ], + "type": "string" + }, + "target_date": { + "description": "Optional project target/end date in YYYY-MM-DD format (e.g., '2026-12-31').", + "pattern": "^\\\\d{4}-\\\\d{2}-\\\\d{2}$", + "type": "string" + } + }, + "required": [ + "project", + "body" + ], + "type": "object" + }, + "name": "create_project_status_update" + }, { "description": "Send a message to Slack (stub for testing)", "inputSchema": { @@ -492,6 +663,45 @@ jobs: } } }, + "create_project_status_update": { + "defaultMax": 10, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65536 + }, + "project": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 512, + "pattern": "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", + "patternError": "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)" + }, + "start_date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "patternError": "must be in YYYY-MM-DD format" + }, + "status": { + "type": "string", + "enum": [ + "INACTIVE", + "ON_TRACK", + "AT_RISK", + "OFF_TRACK", + "COMPLETE" + ] + }, + "target_date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "patternError": "must be in YYYY-MM-DD format" + } + } + }, "missing_tool": { "defaultMax": 20, "fields": { @@ -523,6 +733,43 @@ jobs: "maxLength": 65000 } } + }, + "update_project": { + "defaultMax": 10, + "fields": { + "campaign_id": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "content_number": { + "optionalPositiveInteger": true + }, + "content_type": { + "type": "string", + "enum": [ + "issue", + "pull_request" + ] + }, + "fields": { + "type": "object" + }, + "issue": { + "optionalPositiveInteger": true + }, + "project": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 512, + "pattern": "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+", + "patternError": "must be a full GitHub project URL (e.g., https://github.com/orgs/myorg/projects/42)" + }, + "pull_request": { + "optionalPositiveInteger": true + } + } } } EOF @@ -1861,6 +2108,8 @@ jobs: GH_AW_WORKFLOW_ID: "smoke-copilot" GH_AW_WORKFLOW_NAME: "Smoke Copilot" outputs: + process_project_safe_outputs_processed_count: ${{ steps.process_project_safe_outputs.outputs.processed_count }} + process_project_safe_outputs_temporary_project_map: ${{ steps.process_project_safe_outputs.outputs.temporary_project_map }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: @@ -1874,6 +2123,7 @@ jobs: uses: ./actions/setup with: destination: /opt/gh-aw/actions + safe-output-projects: 'true' - name: Download agent output artifact continue-on-error: true uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 @@ -1885,11 +2135,27 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs/ find "/tmp/gh-aw/safeoutputs/" -type f -print echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process Project-Related Safe Outputs + id: process_project_safe_outputs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_SAFE_OUTPUTS_PROJECT_HANDLER_CONFIG: "{\"create_project_status_update\":{\"max\":5},\"update_project\":{\"max\":20,\"views\":[{\"name\":\"Smoke Test Board\",\"layout\":\"board\",\"filter\":\"is:open\"},{\"name\":\"Smoke Test Table\",\"layout\":\"table\"}]}}" + GH_AW_PROJECT_GITHUB_TOKEN: ${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }} + GH_AW_PROJECT_URL: "https://github.com/orgs/nonexistent-test-org-12345/projects/99999" + with: + github-token: ${{ secrets.GH_AW_PROJECT_GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/safe_output_project_handler_manager.cjs'); + await main(); - name: Process Safe Outputs id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_TEMPORARY_PROJECT_MAP: ${{ steps.process_project_safe_outputs.outputs.temporary_project_map }} 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 }} diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index d92fce0d06..a1d814b4ab 100644 --- a/.github/workflows/smoke-copilot.md +++ b/.github/workflows/smoke-copilot.md @@ -15,6 +15,7 @@ permissions: actions: read name: Smoke Copilot engine: copilot +project: "https://github.com/orgs/nonexistent-test-org-12345/projects/99999" imports: - shared/gh.md - shared/reporting.md @@ -57,6 +58,16 @@ safe-outputs: allowed: [smoke-copilot] remove-labels: allowed: [smoke] + update-project: + max: 20 + views: + - name: "Smoke Test Board" + layout: board + filter: "is:open" + - name: "Smoke Test Table" + layout: table + create-project-status-update: + max: 5 jobs: send-slack-message: description: "Send a message to Slack (stub for testing)" @@ -110,6 +121,83 @@ strict: true - 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 8. **Build gh-aw**: Run `GOCACHE=/tmp/go-cache GOMODCACHE=/tmp/go-mod make build` to verify the agent can successfully build the gh-aw project (both caches must be set to /tmp because the default cache locations are not writable). If the command fails, mark this test as ❌ and report the failure. +9. **Project Operations Testing**: Use project-related safe-output tools to validate multiple project features. All tests use the nonexistent project configured in the frontmatter to ensure no real repositories are affected. Steps: + + a. **Draft Issue Creation**: Call `update_project` with: + - `content_type`: "draft_issue" + - `draft_title`: "Smoke Test Draft Issue - Run ${{ github.run_id }}" + - `draft_body`: "Test draft issue for smoke test validation" + - `fields`: `{"Status": "Todo", "Priority": "High"}` + + b. **Field Creation with New Fields**: Call `update_project` with draft issue including new custom fields: + - `content_type`: "draft_issue" + - `draft_title`: "Smoke Test Draft Issue with Custom Fields - Run ${{ github.run_id }}" + - `fields`: `{"Status": "Todo", "Priority": "High", "Team": "Engineering", "Sprint": "Q1-2026"}` + + c. **Field Update**: Call `update_project` again with the same draft issue to update fields: + - `content_type`: "draft_issue" + - `draft_title`: "Smoke Test Draft Issue - Run ${{ github.run_id }}" + - `fields`: `{"Status": "In Progress", "Priority": "Medium"}` + + d. **Existing Issue Addition**: Use GitHub MCP to find any open issue from ${{ github.repository }}, then call `update_project` with: + - `content_type`: "issue" + - `content_number`: the issue number you found + - `fields`: `{"Status": "In Review", "Priority": "Low"}` + + e. **Existing PR Addition**: Use GitHub MCP to find any open pull request from ${{ github.repository }}, then call `update_project` with: + - `content_type`: "pull_request" + - `content_number`: the PR number you found + - `fields`: `{"Status": "In Progress", "Priority": "High"}` + + f. **View Creation**: The workflow automatically creates two views (configured in safe-outputs): + - "Smoke Test Board" (board layout, filter: "is:open") + - "Smoke Test Table" (table layout) + + g. **Project Status Update**: Call `create_project_status_update` with: + - `body`: "Smoke test project status - Run ${{ github.run_id }}" + - `status`: "ON_TRACK" + + h. **Verification**: For each operation: + - Verify the safe-output message is properly formatted in the output file + - Confirm the project URL auto-populates from frontmatter + - Check that all field names and values are correctly structured + - Validate content_type is correctly set for each operation type + + Note: These tests are expected to fail (the project doesn't exist), which validates that the scope remains within the configured project, message formatting is correct, and no real repositories are polluted. Even though the project operations will fail, the test confirms that real issues and PRs from the repository are correctly referenced in the safe-output messages without actually modifying them. + +10. **Project Scoping Validation**: Test proper scoping behavior with and without top-level project field to ensure operations stay within the correct project scope: + + a. **With Top-Level Project (Default Scoping)**: Call `update_project` WITHOUT specifying a project field in the message: + - `content_type`: "draft_issue" + - `draft_title`: "Scoping Test - Default Project - Run ${{ github.run_id }}" + - `fields`: `{"Status": "Todo"}` + - Verify the message uses the project URL from frontmatter configuration + + b. **Explicit Project Override Attempt**: Call `update_project` WITH an explicit different project field to test that scope is enforced: + - `project`: "https://github.com/orgs/different-org-99999/projects/88888" + - `content_type`: "draft_issue" + - `draft_title`: "Scoping Test - Override Attempt - Run ${{ github.run_id }}" + - `fields`: `{"Status": "Todo"}` + - Verify the message respects the explicit project URL (override should be allowed for flexibility) + + c. **Status Update with Default Project**: Call `create_project_status_update` WITHOUT specifying a project field: + - `body`: "Scoping test status update - Run ${{ github.run_id }}" + - `status`: "AT_RISK" + - Verify the status update uses the project URL from frontmatter + + d. **Status Update with Explicit Project**: Call `create_project_status_update` WITH an explicit project field: + - `project`: "https://github.com/orgs/another-test-org/projects/77777" + - `body`: "Scoping test explicit project - Run ${{ github.run_id }}" + - `status`: "OFF_TRACK" + - Verify the message uses the explicitly provided project URL + + e. **Scoping Verification**: For all operations: + - Confirm that when no project field is provided, the top-level project from frontmatter is used + - Confirm that when an explicit project field is provided, it is used (allowing override) + - Validate that all project URLs are properly formatted in safe-output messages + - Ensure no operations escape to unintended projects + + Note: This test validates that the top-level project field provides a default that auto-populates when not specified, but can be overridden when explicitly provided. All projects are nonexistent to prevent any actual modifications. ## Output