diff --git a/.github/workflows/test-claude.lock.yml b/.github/workflows/test-claude.lock.yml index 728b92880cc..241b1cb83ee 100644 --- a/.github/workflows/test-claude.lock.yml +++ b/.github/workflows/test-claude.lock.yml @@ -37,7 +37,6 @@ jobs: permissions: actions: read contents: read - issues: read pull-requests: write outputs: output: ${{ steps.collect_output.outputs.output }} @@ -456,3 +455,102 @@ 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); + diff --git a/.github/workflows/test-claude.md b/.github/workflows/test-claude.md index 9cdbcc0409a..e2ce5045a33 100644 --- a/.github/workflows/test-claude.md +++ b/.github/workflows/test-claude.md @@ -14,8 +14,11 @@ timeout_minutes: 10 permissions: contents: read pull-requests: write - issues: read actions: read +output: + issue: + title-prefix: "[claude-test] " + labels: [claude, automation, haiku] tools: claude: allowed: diff --git a/Makefile b/Makefile index 7ff4c5ab065..031737e9ac9 100644 --- a/Makefile +++ b/Makefile @@ -102,6 +102,19 @@ validate-workflows: fmt: go fmt ./... +# Run TypeScript compiler on JavaScript files +.PHONY: js +js: + @if command -v tsc >/dev/null 2>&1; then \ + echo "Running TypeScript compiler..."; \ + tsc --noEmit; \ + echo "✓ TypeScript check completed"; \ + else \ + echo "TypeScript compiler (tsc) is not installed. Install it with:"; \ + echo " npm install -g typescript"; \ + echo "Skipping TypeScript check."; \ + fi + # Check formatting .PHONY: fmt-check fmt-check: diff --git a/README.md b/README.md index 8c6ad6d11c4..ab4b32ac63d 100644 --- a/README.md +++ b/README.md @@ -56,12 +56,17 @@ on: types: [opened] permissions: - issues: write + contents: read # Minimal permissions for main job tools: github: allowed: [add_issue_comment] +output: + issue: + title-prefix: "[triage] " + labels: [automation, triage] + timeout_minutes: 5 --- @@ -71,6 +76,7 @@ Analyze issue #${{ github.event.issue.number }} and help with triage: 1. Read the issue content 2. Post a helpful comment summarizing the issue +3. Write your analysis to ${{ env.GITHUB_AW_OUTPUT }} for automatic issue creation Keep responses concise and helpful. ``` diff --git a/docs/frontmatter.md b/docs/frontmatter.md index 3d1e6f2ebbd..957eead872c 100644 --- a/docs/frontmatter.md +++ b/docs/frontmatter.md @@ -25,6 +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 ## Trigger Events (`on:`) @@ -245,6 +246,54 @@ ai-reaction: "eyes" **Note**: Using this feature results in the addition of ".github/actions/reaction/action.yml" file to the repository when the workflow is compiled. +## Output Processing (`output:`) + +Configure automatic output processing from AI agent results: + +```yaml +output: + issue: + title-prefix: "[ai] " # Optional: prefix for issue titles + labels: [automation, ai-agent] # Optional: labels to attach to issues +``` + +**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 +- The agent's output content flows from the main job to the issue creation job via job output variables +- The issue creation job parses the output content, using the first non-empty line as the title and the remainder as the body +- **Important**: With output processing, the main job **does not** need `issues: write` permission since the write operation is performed in the separate job + +**Generated Job Properties:** +- **Job Name**: `create_output_issue` +- **Dependencies**: Runs after the main agent job (`needs: [main-job-name]`) +- **Permissions**: Only the issue creation job has `issues: write` permission +- **Timeout**: 10-minute timeout to prevent hanging +- **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:** +```yaml +--- +on: push +permissions: + contents: read # Main job only needs minimal permissions + actions: read +engine: claude +output: + issue: + title-prefix: "[analysis] " + labels: [automation, code-review] +--- + +# Code Analysis Agent + +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. + ## Cache Configuration (`cache:`) Cache configuration using GitHub Actions `actions/cache` syntax: @@ -384,8 +433,8 @@ on: name: issue-bot permissions: - issues: write - contents: read + contents: read # Main job permissions (no issues: write needed) + actions: read engine: id: claude @@ -394,7 +443,12 @@ engine: tools: github: - allowed: [get_issue, add_issue_comment, update_issue] + allowed: [get_issue, add_issue_comment] + +output: + issue: + title-prefix: "[analysis] " + labels: [automation, ai-analysis] cache: key: deps-${{ hashFiles('**/package-lock.json') }} @@ -420,6 +474,8 @@ if: github.event.issue.state == 'open' Analyze and respond to issues with full context awareness. Current issue text: "${{ needs.task.outputs.text }}" + +Write your analysis to ${{ env.GITHUB_AW_OUTPUT }} for automatic issue creation. ``` ## Related Documentation diff --git a/pkg/cli/templates/instructions.md b/pkg/cli/templates/instructions.md index 635b7434fca..7ac4ffa546f 100644 --- a/pkg/cli/templates/instructions.md +++ b/pkg/cli/templates/instructions.md @@ -73,6 +73,16 @@ The YAML frontmatter supports these fields: - `github:` - GitHub API tools - `claude:` - Claude-specific tools - Custom tool names for MCP servers + +- **`output:`** - Output processing configuration + - `issue:` - Automatic GitHub issue creation from agent output + ```yaml + output: + issue: + title-prefix: "[ai] " # Optional: prefix for issue titles + labels: [automation, ai-agent] # Optional: labels to attach to issues + ``` + **Important**: When using `output.issue`, the main job does **not** need `issues: write` permission since issue creation is handled by a separate job with appropriate permissions. - **`max-turns:`** - Maximum chat iterations per run (integer) - **`stop-time:`** - Deadline for workflow. Can be absolute timestamp ("YYYY-MM-DD HH:MM:SS") or relative delta (+25h, +3d, +1d12h30m). Uses precise date calculations that account for varying month lengths. @@ -118,6 +128,44 @@ cache: Cache steps are automatically added to the workflow job and the cache configuration is removed from the final `.lock.yml` file. +## Output Processing and Issue Creation + +### Automatic GitHub Issue Creation + +Use the `output.issue` configuration to automatically create GitHub issues from AI agent output: + +```yaml +--- +on: push +permissions: + contents: read # Main job only needs minimal permissions + actions: read +engine: claude +output: + issue: + title-prefix: "[analysis] " + labels: [automation, ai-generated] +--- + +# Code Analysis Agent + +Analyze the latest code changes and provide insights. +Write your final analysis to ${{ env.GITHUB_AW_OUTPUT }}. +``` + +**Key Benefits:** +- **Permission Separation**: The main job doesn't need `issues: write` permission +- **Automatic Processing**: AI output is automatically parsed and converted to GitHub issues +- **Job Dependencies**: Issue creation only happens after the AI agent completes successfully +- **Output Variables**: The created issue number and URL are available to downstream jobs + +**How It Works:** +1. AI agent writes output to `${{ env.GITHUB_AW_OUTPUT }}` +2. Main job completes and passes output via job output variables +3. Separate `create_output_issue` job runs with `issues: write` permission +4. JavaScript parses the output (first line = title, rest = body) +5. GitHub issue is created with optional title prefix and labels + ## Trigger Patterns ### Standard GitHub Events @@ -310,11 +358,63 @@ permissions: metadata: read ``` -### Issue Management Pattern +### Direct Issue Management Pattern ```yaml permissions: contents: read issues: write +``` + +### Output Processing Pattern (Recommended) +```yaml +permissions: + contents: read # Main job minimal permissions + actions: read +output: + issue: + title-prefix: "[ai] " + labels: [automation] +``` + +**Note**: With output processing, the main job doesn't need `issues: write` permission. The separate issue creation job automatically gets the required permissions. + +## Output Processing and Issue Creation + +### Automatic GitHub Issue Creation + +Use the `output.issue` configuration to automatically create GitHub issues from AI agent output: + +```yaml +--- +on: push +permissions: + contents: read # Main job only needs minimal permissions + actions: read +engine: claude +output: + issue: + title-prefix: "[analysis] " + labels: [automation, ai-generated] +--- + +# Code Analysis Agent + +Analyze the latest code changes and provide insights. +Write your final analysis to ${{ env.GITHUB_AW_OUTPUT }}. +``` + +**Key Benefits:** +- **Permission Separation**: The main job doesn't need `issues: write` permission +- **Automatic Processing**: AI output is automatically parsed and converted to GitHub issues +- **Job Dependencies**: Issue creation only happens after the AI agent completes successfully +- **Output Variables**: The created issue number and URL are available to downstream jobs + +**How It Works:** +1. AI agent writes output to `${{ env.GITHUB_AW_OUTPUT }}` +2. Main job completes and passes output via job output variables +3. Separate `create_output_issue` job runs with `issues: write` permission +4. JavaScript parses the output (first line = title, rest = body) +5. GitHub issue is created with optional title prefix and labels models: read ``` diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 809fa8d61e0..56ca73bcae8 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -503,6 +503,31 @@ } } ] + }, + "output": { + "type": "object", + "description": "Output configuration for automatic output routes", + "properties": { + "issue": { + "type": "object", + "description": "Configuration for creating GitHub issues from agent output", + "properties": { + "title-prefix": { + "type": "string", + "description": "Optional prefix for the issue title" + }, + "labels": { + "type": "array", + "description": "Optional list of labels to attach to the issue", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index 834c98c0472..0b0b4e2dc89 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -99,6 +99,9 @@ var computeTextActionTemplate string //go:embed templates/check_team_member.yaml var checkTeamMemberTemplate string +//go:embed js/create_issue.js +var createIssueScript string + // Compiler handles converting markdown workflows to GitHub Actions YAML type Compiler struct { verbose bool @@ -208,6 +211,18 @@ type WorkflowData struct { Jobs map[string]any // custom job configurations with dependencies Cache string // cache configuration NeedsTextOutput bool // whether the workflow uses ${{ needs.task.outputs.text }} + Output *OutputConfig // output configuration for automatic output routes +} + +// OutputConfig holds configuration for automatic output routes +type OutputConfig struct { + Issue *IssueConfig `yaml:"issue,omitempty"` +} + +// IssueConfig holds configuration for creating GitHub issues from agent output +type IssueConfig struct { + TitlePrefix string `yaml:"title-prefix,omitempty"` + Labels []string `yaml:"labels,omitempty"` } // CompileWorkflow converts a markdown workflow to GitHub Actions YAML @@ -677,6 +692,9 @@ func (c *Compiler) parseWorkflowFile(markdownPath string) (*WorkflowData, error) workflowData.AIReaction = c.extractYAMLValue(result.Frontmatter, "ai-reaction") workflowData.Jobs = c.extractJobsFromFrontmatter(result.Frontmatter) + // Parse output configuration + workflowData.Output = c.extractOutputConfig(result.Frontmatter) + // Check if "alias" is used as a trigger in the "on" section var hasAlias bool var otherEvents map[string]any @@ -1525,6 +1543,17 @@ func (c *Compiler) buildJobs(data *WorkflowData) error { return fmt.Errorf("failed to add main job: %w", err) } + // Build create_output_issue job if output.issue is configured + if data.Output != nil && data.Output.Issue != nil { + createIssueJob, err := c.buildCreateOutputIssueJob(data) + if err != nil { + return fmt.Errorf("failed to build create_output_issue job: %w", err) + } + if err := c.jobManager.AddJob(createIssueJob); err != nil { + return fmt.Errorf("failed to add create_output_issue 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) @@ -1628,6 +1657,65 @@ func (c *Compiler) buildAddReactionJob(data *WorkflowData) (*Job, error) { return job, nil } +// buildCreateOutputIssueJob creates the create_output_issue job +func (c *Compiler) buildCreateOutputIssueJob(data *WorkflowData) (*Job, error) { + if data.Output == nil || data.Output.Issue == nil { + return nil, fmt.Errorf("output.issue configuration is required") + } + + var steps []string + steps = append(steps, " - name: Create Output Issue\n") + steps = append(steps, " id: create_issue\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)) + if data.Output.Issue.TitlePrefix != "" { + steps = append(steps, fmt.Sprintf(" GITHUB_AW_ISSUE_TITLE_PREFIX: %q\n", data.Output.Issue.TitlePrefix)) + } + if len(data.Output.Issue.Labels) > 0 { + labelsStr := strings.Join(data.Output.Issue.Labels, ",") + steps = append(steps, fmt.Sprintf(" GITHUB_AW_ISSUE_LABELS: %q\n", labelsStr)) + } + + steps = append(steps, " with:\n") + steps = append(steps, " script: |\n") + + // Add each line of the script with proper indentation + scriptLines := strings.Split(createIssueScript, "\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{ + "issue_number": "${{ steps.create_issue.outputs.issue_number }}", + "issue_url": "${{ steps.create_issue.outputs.issue_url }}", + } + + job := &Job{ + Name: "create_output_issue", + If: "", // No conditional execution + RunsOn: "runs-on: ubuntu-latest", + Permissions: "permissions:\n contents: read\n issues: 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 @@ -1970,6 +2058,47 @@ func (c *Compiler) extractJobsFromFrontmatter(frontmatter map[string]any) map[st return make(map[string]any) } +// extractOutputConfig extracts output configuration from frontmatter +func (c *Compiler) extractOutputConfig(frontmatter map[string]any) *OutputConfig { + if output, exists := frontmatter["output"]; exists { + if outputMap, ok := output.(map[string]any); ok { + config := &OutputConfig{} + + // Parse issue configuration + if issue, exists := outputMap["issue"]; exists { + if issueMap, ok := issue.(map[string]any); ok { + issueConfig := &IssueConfig{} + + // Parse title-prefix + if titlePrefix, exists := issueMap["title-prefix"]; exists { + if titlePrefixStr, ok := titlePrefix.(string); ok { + issueConfig.TitlePrefix = titlePrefixStr + } + } + + // Parse labels + if labels, exists := issueMap["labels"]; exists { + if labelsArray, ok := labels.([]any); ok { + var labelStrings []string + for _, label := range labelsArray { + if labelStr, ok := label.(string); ok { + labelStrings = append(labelStrings, labelStr) + } + } + issueConfig.Labels = labelStrings + } + } + + config.Issue = issueConfig + } + } + + return config + } + } + return nil +} + // buildCustomJobs creates custom jobs defined in the frontmatter jobs section func (c *Compiler) buildCustomJobs(data *WorkflowData) error { for jobName, jobConfig := range data.Jobs { diff --git a/pkg/workflow/jobs.go b/pkg/workflow/jobs.go index fe047d394ca..a081d9bbf6b 100644 --- a/pkg/workflow/jobs.go +++ b/pkg/workflow/jobs.go @@ -8,13 +8,14 @@ import ( // Job represents a GitHub Actions job with all its properties type Job struct { - Name string - RunsOn string - If string - Permissions string - Steps []string - Depends []string // Job dependencies (needs clause) - Outputs map[string]string + Name string + RunsOn string + If string + Permissions string + TimeoutMinutes int + Steps []string + Depends []string // Job dependencies (needs clause) + Outputs map[string]string } // JobManager manages a collection of jobs and handles dependency validation @@ -171,6 +172,11 @@ func (jm *JobManager) renderJob(job *Job) string { yaml.WriteString(fmt.Sprintf(" %s\n", job.Permissions)) } + // Add timeout_minutes if specified + if job.TimeoutMinutes > 0 { + yaml.WriteString(fmt.Sprintf(" timeout-minutes: %d\n", job.TimeoutMinutes)) + } + // Add outputs section if len(job.Outputs) > 0 { yaml.WriteString(" outputs:\n") diff --git a/pkg/workflow/js/create_issue.js b/pkg/workflow/js/create_issue.js new file mode 100644 index 00000000000..df2567c89d0 --- /dev/null +++ b/pkg/workflow/js/create_issue.js @@ -0,0 +1,78 @@ +// 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); \ No newline at end of file diff --git a/pkg/workflow/output_test.go b/pkg/workflow/output_test.go new file mode 100644 index 00000000000..e44019ead03 --- /dev/null +++ b/pkg/workflow/output_test.go @@ -0,0 +1,203 @@ +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestOutputConfigParsing(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "output-config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // Test case with output.issue configuration + testContent := `--- +on: push +permissions: + contents: read + issues: write +engine: claude +output: + issue: + title-prefix: "[genai] " + labels: [copilot, automation] +--- + +# Test Output Configuration + +This workflow tests the output configuration parsing. +` + + testFile := filepath.Join(tmpDir, "test-output-config.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 config: %v", err) + } + + // Verify output configuration is parsed correctly + if workflowData.Output == nil { + t.Fatal("Expected output configuration to be parsed") + } + + if workflowData.Output.Issue == nil { + t.Fatal("Expected issue configuration to be parsed") + } + + // Verify title prefix + expectedPrefix := "[genai] " + if workflowData.Output.Issue.TitlePrefix != expectedPrefix { + t.Errorf("Expected title prefix '%s', got '%s'", expectedPrefix, workflowData.Output.Issue.TitlePrefix) + } + + // Verify labels + expectedLabels := []string{"copilot", "automation"} + if len(workflowData.Output.Issue.Labels) != len(expectedLabels) { + t.Errorf("Expected %d labels, got %d", len(expectedLabels), len(workflowData.Output.Issue.Labels)) + } + + for i, expectedLabel := range expectedLabels { + if i >= len(workflowData.Output.Issue.Labels) || workflowData.Output.Issue.Labels[i] != expectedLabel { + t.Errorf("Expected label '%s' at index %d, got '%s'", expectedLabel, i, workflowData.Output.Issue.Labels[i]) + } + } +} + +func TestOutputConfigEmpty(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "output-config-empty-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // Test case without output configuration + testContent := `--- +on: push +permissions: + contents: read + issues: write +engine: claude +--- + +# Test No Output Configuration + +This workflow has no output configuration. +` + + testFile := filepath.Join(tmpDir, "test-no-output.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 without output config: %v", err) + } + + // Verify output configuration is nil + if workflowData.Output != nil { + t.Error("Expected output configuration to be nil when not specified") + } +} + +func TestOutputIssueJobGeneration(t *testing.T) { + // Create temporary directory for test files + tmpDir, err := os.MkdirTemp("", "output-issue-job-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // Test case with output.issue configuration + testContent := `--- +on: push +permissions: + contents: read + issues: write +tools: + github: + allowed: [list_issues] +engine: claude +output: + issue: + title-prefix: "[genai] " + labels: [copilot] +--- + +# Test Output Issue Job Generation + +This workflow tests the create_output_issue job generation. +` + + testFile := filepath.Join(tmpDir, "test-output-issue.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 issue: %v", err) + } + + // Read the generated lock file + lockFile := filepath.Join(tmpDir, "test-output-issue.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_output_issue job exists + if !strings.Contains(lockContent, "create_output_issue:") { + t.Error("Expected 'create_output_issue' job to be in generated workflow") + } + + // Verify job properties + if !strings.Contains(lockContent, "timeout-minutes: 10") { + t.Error("Expected 10-minute timeout in create_output_issue job") + } + + if !strings.Contains(lockContent, "permissions:\n contents: read\n issues: write") { + t.Error("Expected correct permissions in create_output_issue 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_output_issue job") + } + + // Verify JavaScript content includes environment variables for configuration + if !strings.Contains(lockContent, "GITHUB_AW_ISSUE_TITLE_PREFIX: \"[genai] \"") { + t.Error("Expected title prefix to be set as environment variable") + } + + if !strings.Contains(lockContent, "GITHUB_AW_ISSUE_LABELS: \"copilot\"") { + t.Error("Expected copilot label to be set as environment variable") + } + + // Verify job dependencies + if !strings.Contains(lockContent, "needs: test-output-issue") { + t.Error("Expected create_output_issue job to depend on main job") + } + + t.Logf("Generated workflow content:\n%s", lockContent) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000000..56d0277c762 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "esnext", + "lib": ["es2022", "dom"], + "allowJs": true, + "checkJs": false, + "declaration": false, + "outDir": "./dist/js", + "rootDir": "./pkg/workflow/js", + "strict": false, + "noImplicitAny": false, + "strictNullChecks": false, + "strictFunctionTypes": false, + "noImplicitThis": false, + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": false, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "noEmit": true + }, + "include": [ + "pkg/workflow/js/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file