From 5d963db3d005b5d538adfd4e76c4923eb64d5faf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 06:38:40 +0000 Subject: [PATCH 1/4] Initial plan From 954545aa493a68593c3e833dbc800c30bb8c8a7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 06:56:48 +0000 Subject: [PATCH 2/4] Implement output.comment functionality for issue/PR comments Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/frontmatter.md | 48 ++++- pkg/parser/schemas/main_workflow_schema.json | 5 + pkg/workflow/compiler.go | 82 +++++++- pkg/workflow/js/create_comment.js | 66 ++++++ pkg/workflow/output_test.go | 199 +++++++++++++++++++ 5 files changed, 396 insertions(+), 4 deletions(-) create mode 100644 pkg/workflow/js/create_comment.js diff --git a/docs/frontmatter.md b/docs/frontmatter.md index 957eead872c..57731739655 100644 --- a/docs/frontmatter.md +++ b/docs/frontmatter.md @@ -25,7 +25,7 @@ The YAML frontmatter supports standard GitHub Actions properties plus additional - `alias`: Alias name for the workflow - `ai-reaction`: Emoji reaction to add/remove on triggering GitHub item - `cache`: Cache configuration for workflow dependencies -- `output`: Output processing configuration for automatic issue creation +- `output`: Output processing configuration for automatic issue creation and comment posting ## Trigger Events (`on:`) @@ -255,8 +255,11 @@ output: issue: title-prefix: "[ai] " # Optional: prefix for issue titles labels: [automation, ai-agent] # Optional: labels to attach to issues + comment: {} # Create comments on issues/PRs from agent output ``` +### Issue Creation (`output.issue`) + **Behavior:** - When `output.issue` is configured, the compiler automatically generates a separate `create_output_issue` job - This job runs after the main AI agent job completes @@ -272,7 +275,24 @@ output: - **Environment Variables**: Configuration passed via `GITHUB_AW_ISSUE_TITLE_PREFIX` and `GITHUB_AW_ISSUE_LABELS` - **Outputs**: Returns `issue_number` and `issue_url` for downstream jobs -**Example workflow using output processing:** +### Comment Creation (`output.comment`) + +**Behavior:** +- When `output.comment` is configured, the compiler automatically generates a separate `create_issue_comment` job +- This job runs after the main AI agent job completes and **only** if the workflow is triggered by an issue or pull request event +- The agent's output content flows from the main job to the comment creation job via job output variables +- The comment creation job posts the entire agent output as a comment on the triggering issue or pull request +- **Conditional Execution**: The job automatically skips if not running in an issue or pull request context + +**Generated Job Properties:** +- **Job Name**: `create_issue_comment` +- **Dependencies**: Runs after the main agent job (`needs: [main-job-name]`) +- **Conditional**: Only runs when `github.event.issue.number || github.event.pull_request.number` is present +- **Permissions**: Only the comment creation job has `issues: write` and `pull-requests: write` permissions +- **Timeout**: 10-minute timeout to prevent hanging +- **Outputs**: Returns `comment_id` and `comment_url` for downstream jobs + +**Example workflow using issue creation:** ```yaml --- on: push @@ -292,7 +312,29 @@ Analyze the latest commit and provide insights. Write your analysis to ${{ env.GITHUB_AW_OUTPUT }} at the end. ``` -This automatically creates GitHub issues from the agent's analysis without requiring `issues: write` permission on the main job. +**Example workflow using comment creation:** +```yaml +--- +on: + issues: + types: [opened, labeled] + pull_request: + types: [opened, synchronize] +permissions: + contents: read # Main job only needs minimal permissions + actions: read +engine: claude +output: + comment: {} +--- + +# Issue/PR Analysis Agent + +Analyze the issue or pull request and provide feedback. +Write your analysis to ${{ env.GITHUB_AW_OUTPUT }} at the end. +``` + +This automatically creates GitHub issues or comments from the agent's analysis without requiring write permissions on the main job. ## Cache Configuration (`cache:`) diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 56ca73bcae8..02f3dea895e 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -525,6 +525,11 @@ } }, "additionalProperties": false + }, + "comment": { + "type": "object", + "description": "Configuration for creating GitHub issue/PR comments from agent output", + "additionalProperties": false } }, "additionalProperties": false diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 0b0b4e2dc89..e1c5eb62b38 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -102,6 +102,9 @@ var checkTeamMemberTemplate string //go:embed js/create_issue.js var createIssueScript string +//go:embed js/create_comment.js +var createCommentScript string + // Compiler handles converting markdown workflows to GitHub Actions YAML type Compiler struct { verbose bool @@ -216,7 +219,8 @@ type WorkflowData struct { // OutputConfig holds configuration for automatic output routes type OutputConfig struct { - Issue *IssueConfig `yaml:"issue,omitempty"` + Issue *IssueConfig `yaml:"issue,omitempty"` + Comment *CommentConfig `yaml:"comment,omitempty"` } // IssueConfig holds configuration for creating GitHub issues from agent output @@ -225,6 +229,11 @@ type IssueConfig struct { Labels []string `yaml:"labels,omitempty"` } +// CommentConfig holds configuration for creating GitHub issue/PR comments from agent output +type CommentConfig struct { + // Empty struct for now, as per requirements, but structured for future expansion +} + // CompileWorkflow converts a markdown workflow to GitHub Actions YAML func (c *Compiler) CompileWorkflow(markdownPath string) error { @@ -1554,6 +1563,17 @@ func (c *Compiler) buildJobs(data *WorkflowData) error { } } + // Build create_issue_comment job if output.comment is configured + if data.Output != nil && data.Output.Comment != nil { + createCommentJob, err := c.buildCreateOutputCommentJob(data) + if err != nil { + return fmt.Errorf("failed to build create_issue_comment job: %w", err) + } + if err := c.jobManager.AddJob(createCommentJob); err != nil { + return fmt.Errorf("failed to add create_issue_comment job: %w", err) + } + } + // Build additional custom jobs from frontmatter jobs section if err := c.buildCustomJobs(data); err != nil { return fmt.Errorf("failed to build custom jobs: %w", err) @@ -1716,6 +1736,58 @@ func (c *Compiler) buildCreateOutputIssueJob(data *WorkflowData) (*Job, error) { return job, nil } +// buildCreateOutputCommentJob creates the create_issue_comment job +func (c *Compiler) buildCreateOutputCommentJob(data *WorkflowData) (*Job, error) { + if data.Output == nil || data.Output.Comment == nil { + return nil, fmt.Errorf("output.comment configuration is required") + } + + var steps []string + steps = append(steps, " - name: Create Output Comment\n") + steps = append(steps, " id: create_comment\n") + steps = append(steps, " uses: actions/github-script@v7\n") + + // Determine the main job name to get output from + mainJobName := c.generateJobName(data.Name) + + // Add environment variables + steps = append(steps, " env:\n") + // Pass the agent output content from the main job + steps = append(steps, fmt.Sprintf(" AGENT_OUTPUT_CONTENT: ${{ needs.%s.outputs.output }}\n", mainJobName)) + + steps = append(steps, " with:\n") + steps = append(steps, " script: |\n") + + // Add each line of the script with proper indentation + scriptLines := strings.Split(createCommentScript, "\n") + for _, line := range scriptLines { + if strings.TrimSpace(line) == "" { + steps = append(steps, "\n") + } else { + steps = append(steps, fmt.Sprintf(" %s\n", line)) + } + } + + // Create outputs for the job + outputs := map[string]string{ + "comment_id": "${{ steps.create_comment.outputs.comment_id }}", + "comment_url": "${{ steps.create_comment.outputs.comment_url }}", + } + + job := &Job{ + Name: "create_issue_comment", + If: "if: github.event.issue.number || github.event.pull_request.number", // Only run in issue or PR context + RunsOn: "runs-on: ubuntu-latest", + Permissions: "permissions:\n contents: read\n issues: write\n pull-requests: write", + TimeoutMinutes: 10, // 10-minute timeout as required + Steps: steps, + Outputs: outputs, + Depends: []string{mainJobName}, // Depend on the main workflow job + } + + return job, nil +} + // buildMainJob creates the main workflow job func (c *Compiler) buildMainJob(data *WorkflowData, jobName string) (*Job, error) { var steps []string @@ -2093,6 +2165,14 @@ func (c *Compiler) extractOutputConfig(frontmatter map[string]any) *OutputConfig } } + // Parse comment configuration + if comment, exists := outputMap["comment"]; exists { + if _, ok := comment.(map[string]any); ok { + // For now, CommentConfig is an empty struct + config.Comment = &CommentConfig{} + } + } + return config } } diff --git a/pkg/workflow/js/create_comment.js b/pkg/workflow/js/create_comment.js new file mode 100644 index 00000000000..646e74b7d64 --- /dev/null +++ b/pkg/workflow/js/create_comment.js @@ -0,0 +1,66 @@ +// Read the agent output content from environment variable +const outputContent = process.env.AGENT_OUTPUT_CONTENT; +if (!outputContent) { + console.log('No AGENT_OUTPUT_CONTENT environment variable found'); + return; +} + +if (outputContent.trim() === '') { + console.log('Agent output content is empty'); + return; +} + +console.log('Agent output content length:', outputContent.length); + +// Check if we're in an issue or pull request context +const isIssueContext = context.eventName === 'issues' || context.eventName === 'issue_comment'; +const isPRContext = context.eventName === 'pull_request' || context.eventName === 'pull_request_review' || context.eventName === 'pull_request_review_comment'; + +if (!isIssueContext && !isPRContext) { + console.log('Not running in issue or pull request context, skipping comment creation'); + return; +} + +// Determine the issue/PR number and comment endpoint +let issueNumber; +let commentEndpoint; + +if (isIssueContext) { + if (context.payload.issue) { + issueNumber = context.payload.issue.number; + commentEndpoint = 'issues'; + } else { + console.log('Issue context detected but no issue found in payload'); + return; + } +} else if (isPRContext) { + if (context.payload.pull_request) { + issueNumber = context.payload.pull_request.number; + commentEndpoint = 'issues'; // PR comments use the issues API endpoint + } else { + console.log('Pull request context detected but no pull request found in payload'); + return; + } +} + +if (!issueNumber) { + console.log('Could not determine issue or pull request number'); + return; +} + +console.log(`Creating comment on ${commentEndpoint} #${issueNumber}`); +console.log('Comment content length:', outputContent.length); + +// Create the comment using GitHub API +const { data: comment } = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: outputContent +}); + +console.log('Created comment #' + comment.id + ': ' + comment.html_url); + +// Set output for other jobs to use +core.setOutput('comment_id', comment.id); +core.setOutput('comment_url', comment.html_url); \ No newline at end of file diff --git a/pkg/workflow/output_test.go b/pkg/workflow/output_test.go index e44019ead03..f6881ac9522 100644 --- a/pkg/workflow/output_test.go +++ b/pkg/workflow/output_test.go @@ -201,3 +201,202 @@ This workflow tests the create_output_issue job generation. t.Logf("Generated workflow content:\n%s", lockContent) } + +func TestOutputCommentConfigParsing(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "output-comment-config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // Test case with output.comment configuration + testContent := `--- +on: + issues: + types: [opened] +permissions: + contents: read + issues: write + pull-requests: write +engine: claude +output: + comment: {} +--- + +# Test Output Comment Configuration + +This workflow tests the output.comment configuration parsing. +` + + testFile := filepath.Join(tmpDir, "test-output-comment.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + compiler := NewCompiler(false, "", "test") + + // Parse the workflow data + workflowData, err := compiler.parseWorkflowFile(testFile) + if err != nil { + t.Fatalf("Unexpected error parsing workflow with output comment config: %v", err) + } + + // Verify output configuration is parsed correctly + if workflowData.Output == nil { + t.Fatal("Expected output configuration to be parsed") + } + + if workflowData.Output.Comment == nil { + t.Fatal("Expected comment configuration to be parsed") + } +} + +func TestOutputCommentJobGeneration(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "output-comment-job-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // Test case with output.comment configuration + testContent := `--- +on: + issues: + types: [opened] +permissions: + contents: read + issues: write + pull-requests: write +tools: + github: + allowed: [get_issue] +engine: claude +output: + comment: {} +--- + +# Test Output Comment Job Generation + +This workflow tests the create_issue_comment job generation. +` + + testFile := filepath.Join(tmpDir, "test-output-comment.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + compiler := NewCompiler(false, "", "test") + + // Compile the workflow + err = compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Unexpected error compiling workflow with output comment: %v", err) + } + + // Read the generated lock file + lockFile := filepath.Join(tmpDir, "test-output-comment.lock.yml") + content, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + lockContent := string(content) + + // Verify create_issue_comment job exists + if !strings.Contains(lockContent, "create_issue_comment:") { + t.Error("Expected 'create_issue_comment' job to be in generated workflow") + } + + // Verify job properties + if !strings.Contains(lockContent, "timeout-minutes: 10") { + t.Error("Expected 10-minute timeout in create_issue_comment job") + } + + if !strings.Contains(lockContent, "permissions:\n contents: read\n issues: write\n pull-requests: write") { + t.Error("Expected correct permissions in create_issue_comment job") + } + + // Verify the job uses github-script + if !strings.Contains(lockContent, "uses: actions/github-script@v7") { + t.Error("Expected github-script action to be used in create_issue_comment job") + } + + // Verify job has conditional execution + if !strings.Contains(lockContent, "if: github.event.issue.number || github.event.pull_request.number") { + t.Error("Expected create_issue_comment job to have conditional execution") + } + + // Verify job dependencies + if !strings.Contains(lockContent, "needs: test-output-comment") { + t.Error("Expected create_issue_comment job to depend on main job") + } + + // Verify JavaScript content includes environment variable for agent output + if !strings.Contains(lockContent, "AGENT_OUTPUT_CONTENT:") { + t.Error("Expected agent output content to be passed as environment variable") + } + + t.Logf("Generated workflow content:\n%s", lockContent) +} + +func TestOutputCommentJobSkippedForNonIssueEvents(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "output-comment-skip-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // Test case with output.comment configuration but push trigger (not issue/PR) + testContent := `--- +on: push +permissions: + contents: read + issues: write + pull-requests: write +engine: claude +output: + comment: {} +--- + +# Test Output Comment Job Skipping + +This workflow tests that comment job is skipped for non-issue/PR events. +` + + testFile := filepath.Join(tmpDir, "test-comment-skip.md") + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatal(err) + } + + compiler := NewCompiler(false, "", "test") + + // Compile the workflow + err = compiler.CompileWorkflow(testFile) + if err != nil { + t.Fatalf("Unexpected error compiling workflow with output comment: %v", err) + } + + // Read the generated lock file + lockFile := filepath.Join(tmpDir, "test-comment-skip.lock.yml") + content, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + lockContent := string(content) + + // Verify create_issue_comment job exists (it should be generated regardless of trigger) + if !strings.Contains(lockContent, "create_issue_comment:") { + t.Error("Expected 'create_issue_comment' job to be in generated workflow") + } + + // Verify job has conditional execution to skip when not in issue/PR context + if !strings.Contains(lockContent, "if: github.event.issue.number || github.event.pull_request.number") { + t.Error("Expected create_issue_comment job to have conditional execution for skipping") + } + + t.Logf("Generated workflow content:\n%s", lockContent) +} From 7fc8e8d11b6675ca614257437ded53ff7ec03a57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:47:46 +0000 Subject: [PATCH 3/4] Update test-claude.md to use output.comment instead of output.issue Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/test-claude.lock.yml | 98 +++++++++++--------------- .github/workflows/test-claude.md | 4 +- 2 files changed, 44 insertions(+), 58 deletions(-) diff --git a/.github/workflows/test-claude.lock.yml b/.github/workflows/test-claude.lock.yml index 241b1cb83ee..73563a9e3e7 100644 --- a/.github/workflows/test-claude.lock.yml +++ b/.github/workflows/test-claude.lock.yml @@ -455,24 +455,24 @@ jobs: path: /tmp/aw_info.json if-no-files-found: warn - create_output_issue: + create_issue_comment: needs: test-claude + if: github.event.issue.number || github.event.pull_request.number runs-on: ubuntu-latest permissions: contents: read issues: write + pull-requests: write timeout-minutes: 10 outputs: - issue_number: ${{ steps.create_issue.outputs.issue_number }} - issue_url: ${{ steps.create_issue.outputs.issue_url }} + comment_id: ${{ steps.create_comment.outputs.comment_id }} + comment_url: ${{ steps.create_comment.outputs.comment_url }} steps: - - name: Create Output Issue - id: create_issue + - name: Create Output Comment + id: create_comment uses: actions/github-script@v7 env: AGENT_OUTPUT_CONTENT: ${{ needs.test-claude.outputs.output }} - GITHUB_AW_ISSUE_TITLE_PREFIX: "[claude-test] " - GITHUB_AW_ISSUE_LABELS: "claude,automation,haiku" with: script: | // Read the agent output content from environment variable @@ -489,68 +489,56 @@ jobs: console.log('Agent output content length:', outputContent.length); - // Parse the output to extract title and body - const lines = outputContent.split('\n'); - let title = ''; - let bodyLines = []; - let foundTitle = false; + // Check if we're in an issue or pull request context + const isIssueContext = context.eventName === 'issues' || context.eventName === 'issue_comment'; + const isPRContext = context.eventName === 'pull_request' || context.eventName === 'pull_request_review' || context.eventName === 'pull_request_review_comment'; - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); + if (!isIssueContext && !isPRContext) { + console.log('Not running in issue or pull request context, skipping comment creation'); + return; + } - // Skip empty lines until we find the title - if (!foundTitle && line === '') { - continue; - } + // Determine the issue/PR number and comment endpoint + let issueNumber; + let commentEndpoint; - // First non-empty line becomes the title - if (!foundTitle && line !== '') { - // Remove markdown heading syntax if present - title = line.replace(/^#+\s*/, '').trim(); - foundTitle = true; - continue; + if (isIssueContext) { + if (context.payload.issue) { + issueNumber = context.payload.issue.number; + commentEndpoint = 'issues'; + } else { + console.log('Issue context detected but no issue found in payload'); + return; } - - // Everything else goes into the body - if (foundTitle) { - bodyLines.push(lines[i]); // Keep original formatting + } else if (isPRContext) { + if (context.payload.pull_request) { + issueNumber = context.payload.pull_request.number; + commentEndpoint = 'issues'; // PR comments use the issues API endpoint + } else { + console.log('Pull request context detected but no pull request found in payload'); + return; } } - // If no title was found, use a default - if (!title) { - title = 'Agent Output'; - } - - // Apply title prefix if provided via environment variable - const titlePrefix = process.env.GITHUB_AW_ISSUE_TITLE_PREFIX; - if (titlePrefix && !title.startsWith(titlePrefix)) { - title = titlePrefix + title; + if (!issueNumber) { + console.log('Could not determine issue or pull request number'); + return; } - // Prepare the body content - const body = bodyLines.join('\n').trim(); - - // Parse labels from environment variable (comma-separated string) - const labelsEnv = process.env.GITHUB_AW_ISSUE_LABELS; - const labels = labelsEnv ? labelsEnv.split(',').map(label => label.trim()).filter(label => label) : []; - - console.log('Creating issue with title:', title); - console.log('Labels:', labels); - console.log('Body length:', body.length); + console.log(`Creating comment on ${commentEndpoint} #${issueNumber}`); + console.log('Comment content length:', outputContent.length); - // Create the issue using GitHub API - const { data: issue } = await github.rest.issues.create({ + // Create the comment using GitHub API + const { data: comment } = await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, - title: title, - body: body, - labels: labels + issue_number: issueNumber, + body: outputContent }); - console.log('Created issue #' + issue.number + ': ' + issue.html_url); + console.log('Created comment #' + comment.id + ': ' + comment.html_url); // Set output for other jobs to use - core.setOutput('issue_number', issue.number); - core.setOutput('issue_url', issue.html_url); + core.setOutput('comment_id', comment.id); + core.setOutput('comment_url', comment.html_url); diff --git a/.github/workflows/test-claude.md b/.github/workflows/test-claude.md index e2ce5045a33..e31cea93c7b 100644 --- a/.github/workflows/test-claude.md +++ b/.github/workflows/test-claude.md @@ -16,9 +16,7 @@ permissions: pull-requests: write actions: read output: - issue: - title-prefix: "[claude-test] " - labels: [claude, automation, haiku] + comment: {} tools: claude: allowed: From fea0dd32d42a9694b1cad052ee86509046c97cb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:59:54 +0000 Subject: [PATCH 4/4] Add both issue and comment output options to test-claude.md Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/test-claude.lock.yml | 99 ++++++++++++++++++++++++++ .github/workflows/test-claude.md | 3 + 2 files changed, 102 insertions(+) diff --git a/.github/workflows/test-claude.lock.yml b/.github/workflows/test-claude.lock.yml index 73563a9e3e7..5eb990fac9c 100644 --- a/.github/workflows/test-claude.lock.yml +++ b/.github/workflows/test-claude.lock.yml @@ -455,6 +455,105 @@ jobs: path: /tmp/aw_info.json if-no-files-found: warn + create_output_issue: + needs: test-claude + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + timeout-minutes: 10 + outputs: + issue_number: ${{ steps.create_issue.outputs.issue_number }} + issue_url: ${{ steps.create_issue.outputs.issue_url }} + steps: + - name: Create Output Issue + id: create_issue + uses: actions/github-script@v7 + env: + AGENT_OUTPUT_CONTENT: ${{ needs.test-claude.outputs.output }} + GITHUB_AW_ISSUE_TITLE_PREFIX: "[claude-test] " + GITHUB_AW_ISSUE_LABELS: "claude,automation,haiku" + with: + script: | + // Read the agent output content from environment variable + const outputContent = process.env.AGENT_OUTPUT_CONTENT; + if (!outputContent) { + console.log('No AGENT_OUTPUT_CONTENT environment variable found'); + return; + } + + if (outputContent.trim() === '') { + console.log('Agent output content is empty'); + return; + } + + console.log('Agent output content length:', outputContent.length); + + // Parse the output to extract title and body + const lines = outputContent.split('\n'); + let title = ''; + let bodyLines = []; + let foundTitle = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // Skip empty lines until we find the title + if (!foundTitle && line === '') { + continue; + } + + // First non-empty line becomes the title + if (!foundTitle && line !== '') { + // Remove markdown heading syntax if present + title = line.replace(/^#+\s*/, '').trim(); + foundTitle = true; + continue; + } + + // Everything else goes into the body + if (foundTitle) { + bodyLines.push(lines[i]); // Keep original formatting + } + } + + // If no title was found, use a default + if (!title) { + title = 'Agent Output'; + } + + // Apply title prefix if provided via environment variable + const titlePrefix = process.env.GITHUB_AW_ISSUE_TITLE_PREFIX; + if (titlePrefix && !title.startsWith(titlePrefix)) { + title = titlePrefix + title; + } + + // Prepare the body content + const body = bodyLines.join('\n').trim(); + + // Parse labels from environment variable (comma-separated string) + const labelsEnv = process.env.GITHUB_AW_ISSUE_LABELS; + const labels = labelsEnv ? labelsEnv.split(',').map(label => label.trim()).filter(label => label) : []; + + console.log('Creating issue with title:', title); + console.log('Labels:', labels); + console.log('Body length:', body.length); + + // Create the issue using GitHub API + const { data: issue } = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + body: body, + labels: labels + }); + + console.log('Created issue #' + issue.number + ': ' + issue.html_url); + + // Set output for other jobs to use + core.setOutput('issue_number', issue.number); + core.setOutput('issue_url', issue.html_url); + create_issue_comment: needs: test-claude if: github.event.issue.number || github.event.pull_request.number diff --git a/.github/workflows/test-claude.md b/.github/workflows/test-claude.md index e31cea93c7b..30b9ccc86d7 100644 --- a/.github/workflows/test-claude.md +++ b/.github/workflows/test-claude.md @@ -16,6 +16,9 @@ permissions: pull-requests: write actions: read output: + issue: + title-prefix: "[claude-test] " + labels: [claude, automation, haiku] comment: {} tools: claude: