Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/workflows/test-claude.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .github/workflows/test-claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ output:
issue:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot keep the issue section, output should support multiple options

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated test-claude.md to keep both issue and comment output options as requested. The workflow now supports multiple output types simultaneously, demonstrating the full functionality. Commit: fea0dd3

title-prefix: "[claude-test] "
labels: [claude, automation, haiku]
comment: {}
tools:
claude:
allowed:
Expand Down
48 changes: 45 additions & 3 deletions docs/frontmatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:`)

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:`)

Expand Down
5 changes: 5 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,11 @@
}
},
"additionalProperties": false
},
"comment": {
"type": "object",
"description": "Configuration for creating GitHub issue/PR comments from agent output",
"additionalProperties": false
}
},
"additionalProperties": false
Expand Down
82 changes: 81 additions & 1 deletion pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2123,6 +2195,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
}
}
Expand Down
66 changes: 66 additions & 0 deletions pkg/workflow/js/create_comment.js
Original file line number Diff line number Diff line change
@@ -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);
Loading