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
67 changes: 0 additions & 67 deletions .github/actions/compute-text/action.yml

This file was deleted.

5 changes: 0 additions & 5 deletions .github/workflows/test-codex.lock.yml

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

5 changes: 0 additions & 5 deletions .github/workflows/weekly-research.lock.yml

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

60 changes: 42 additions & 18 deletions pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ type WorkflowData struct {
AIReaction string // AI reaction type like "eyes", "heart", etc.
Jobs map[string]any // custom job configurations with dependencies
Cache string // cache configuration
NeedsTextOutput bool // whether the workflow uses ${{ needs.task.outputs.text }}
}

// CompileWorkflow converts a markdown workflow to GitHub Actions YAML
Expand Down Expand Up @@ -192,18 +193,20 @@ func (c *Compiler) CompileWorkflow(markdownPath string) error {
return errors.New(formattedErr)
}

// Write shared compute-text action (always generated for task job)
if err := c.writeComputeTextAction(markdownPath); err != nil {
formattedErr := console.FormatError(console.CompilerError{
Position: console.ErrorPosition{
File: markdownPath,
Line: 1,
Column: 1,
},
Type: "error",
Message: fmt.Sprintf("failed to write compute-text action: %v", err),
})
return errors.New(formattedErr)
// Write shared compute-text action (only if needed for task job)
if workflowData.NeedsTextOutput {
if err := c.writeComputeTextAction(markdownPath); err != nil {
formattedErr := console.FormatError(console.CompilerError{
Position: console.ErrorPosition{
File: markdownPath,
Line: 1,
Column: 1,
},
Type: "error",
Message: fmt.Sprintf("failed to write compute-text action: %v", err),
})
return errors.New(formattedErr)
}
}

// Write shared check-team-member action (only for alias workflows)
Expand Down Expand Up @@ -516,13 +519,17 @@ func (c *Compiler) parseWorkflowFile(markdownPath string) (*WorkflowData, error)
fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Extracted workflow name: '%s'", workflowName)))
}

// Check if the markdown content uses the text output
needsTextOutput := c.detectTextOutputUsage(markdownContent)

// Build workflow data
workflowData := &WorkflowData{
Name: workflowName,
Tools: tools,
MarkdownContent: markdownContent,
AI: engineSetting,
EngineConfig: engineConfig,
NeedsTextOutput: needsTextOutput,
}

// Extract YAML sections from frontmatter - use direct frontmatter map extraction
Expand Down Expand Up @@ -1140,6 +1147,20 @@ func (c *Compiler) applyDefaultGitHubMCPTools(tools map[string]any) map[string]a
return tools
}

// detectTextOutputUsage checks if the markdown content uses ${{ needs.task.outputs.text }}
func (c *Compiler) detectTextOutputUsage(markdownContent string) bool {
// Check for the specific GitHub Actions expression
hasUsage := strings.Contains(markdownContent, "${{ needs.task.outputs.text }}")
if c.verbose {
if hasUsage {
fmt.Println(console.FormatInfoMessage("Detected usage of task.outputs.text - compute-text step will be included"))
} else {
fmt.Println(console.FormatInfoMessage("No usage of task.outputs.text found - compute-text step will be skipped"))
}
}
return hasUsage
}

// computeAllowedTools computes the comma-separated list of allowed tools for Claude
func (c *Compiler) computeAllowedTools(tools map[string]any) string {
var allowedTools []string
Expand Down Expand Up @@ -1413,14 +1434,17 @@ func (c *Compiler) buildTaskJob(data *WorkflowData) (*Job, error) {
steps = append(steps, " exit 1\n")
}

// Use shared compute-text action
steps = append(steps, " - name: Compute current body text\n")
steps = append(steps, " id: compute-text\n")
steps = append(steps, " uses: ./.github/actions/compute-text\n")
// Use shared compute-text action only if needed
if data.NeedsTextOutput {
steps = append(steps, " - name: Compute current body text\n")
steps = append(steps, " id: compute-text\n")
steps = append(steps, " uses: ./.github/actions/compute-text\n")
}

// Set up outputs
outputs := map[string]string{
"text": "${{ steps.compute-text.outputs.text }}",
outputs := map[string]string{}
if data.NeedsTextOutput {
outputs["text"] = "${{ steps.compute-text.outputs.text }}"
}

job := &Job{
Expand Down
177 changes: 177 additions & 0 deletions pkg/workflow/compute_text_lazy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package workflow

import (
"os"
"path/filepath"
"strings"
"testing"
)

func TestComputeTextLazyInsertion(t *testing.T) {
// Create a temporary directory for the test
tempDir, err := os.MkdirTemp("", "compute-text-lazy-test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)

// Create a .git directory to simulate a git repository
gitDir := filepath.Join(tempDir, ".git")
if err := os.MkdirAll(gitDir, 0755); err != nil {
t.Fatalf("Failed to create .git dir: %v", err)
}

// Test case 1: Workflow that uses task.outputs.text
workflowWithText := `---
on:
issues:
types: [opened]
permissions:
issues: write
tools:
github:
allowed: [add_issue_comment]
---

# Test Workflow With Text Output

This workflow uses the text output: "${{ needs.task.outputs.text }}"

Please analyze this issue and provide a helpful response.`

workflowWithTextPath := filepath.Join(tempDir, "with-text.md")
if err := os.WriteFile(workflowWithTextPath, []byte(workflowWithText), 0644); err != nil {
t.Fatalf("Failed to write workflow with text: %v", err)
}

// Test case 2: Workflow that does NOT use task.outputs.text
workflowWithoutText := `---
on:
schedule:
- cron: "0 9 * * 1"
permissions:
issues: write
tools:
github:
allowed: [create_issue]
---

# Test Workflow Without Text Output

This workflow does NOT use the text output.

Create a report based on repository analysis.`

workflowWithoutTextPath := filepath.Join(tempDir, "without-text.md")
if err := os.WriteFile(workflowWithoutTextPath, []byte(workflowWithoutText), 0644); err != nil {
t.Fatalf("Failed to write workflow without text: %v", err)
}

compiler := NewCompiler(false, "", "test-version")

// Test workflow WITH text usage
t.Run("workflow_with_text_usage", func(t *testing.T) {
err := compiler.CompileWorkflow(workflowWithTextPath)
if err != nil {
t.Fatalf("Failed to compile workflow with text: %v", err)
}

// Check that compute-text action was created
actionPath := filepath.Join(tempDir, ".github", "actions", "compute-text", "action.yml")
if _, err := os.Stat(actionPath); os.IsNotExist(err) {
t.Error("Expected compute-text action to be created for workflow that uses text output")
}

// Check that the compiled YAML contains compute-text step
lockPath := strings.TrimSuffix(workflowWithTextPath, ".md") + ".lock.yml"
lockContent, err := os.ReadFile(lockPath)
if err != nil {
t.Fatalf("Failed to read compiled workflow: %v", err)
}

lockStr := string(lockContent)
if !strings.Contains(lockStr, "compute-text") {
t.Error("Expected compiled workflow to contain compute-text step")
}
if !strings.Contains(lockStr, "text: ${{ steps.compute-text.outputs.text }}") {
t.Error("Expected compiled workflow to contain text output")
}
})

// Remove compute-text action for next test
os.RemoveAll(filepath.Join(tempDir, ".github"))

// Test workflow WITHOUT text usage
t.Run("workflow_without_text_usage", func(t *testing.T) {
err := compiler.CompileWorkflow(workflowWithoutTextPath)
if err != nil {
t.Fatalf("Failed to compile workflow without text: %v", err)
}

// Check that compute-text action was NOT created
actionPath := filepath.Join(tempDir, ".github", "actions", "compute-text", "action.yml")
if _, err := os.Stat(actionPath); !os.IsNotExist(err) {
t.Error("Expected compute-text action NOT to be created for workflow that doesn't use text output")
}

// Check that the compiled YAML does NOT contain compute-text step
lockPath := strings.TrimSuffix(workflowWithoutTextPath, ".md") + ".lock.yml"
lockContent, err := os.ReadFile(lockPath)
if err != nil {
t.Fatalf("Failed to read compiled workflow: %v", err)
}

lockStr := string(lockContent)
if strings.Contains(lockStr, "compute-text") {
t.Error("Expected compiled workflow NOT to contain compute-text step")
}
if strings.Contains(lockStr, "text: ${{ steps.compute-text.outputs.text }}") {
t.Error("Expected compiled workflow NOT to contain text output")
}
})
}

func TestDetectTextOutputUsage(t *testing.T) {
compiler := NewCompiler(false, "", "test-version")

tests := []struct {
name string
content string
expectedUsage bool
}{
{
name: "with_text_usage",
content: "Analyze this: \"${{ needs.task.outputs.text }}\"",
expectedUsage: true,
},
{
name: "without_text_usage",
content: "Create a report based on repository analysis.",
expectedUsage: false,
},
{
name: "with_other_github_expressions",
content: "Repository: ${{ github.repository }} but no text output",
expectedUsage: false,
},
{
name: "with_partial_match",
content: "Something about task.outputs but not the full expression",
expectedUsage: false,
},
{
name: "with_multiple_usages",
content: "First: \"${{ needs.task.outputs.text }}\" and second: \"${{ needs.task.outputs.text }}\"",
expectedUsage: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := compiler.detectTextOutputUsage(tt.content)
if result != tt.expectedUsage {
t.Errorf("detectTextOutputUsage() = %v, expected %v", result, tt.expectedUsage)
}
})
}
}