From 7a5ddd0d7889006d3463d71f41760f1d64fe7c98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:51:36 +0000 Subject: [PATCH 1/3] Initial plan From db743bafe503f522ea8bff7c205213597c9912bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:05:42 +0000 Subject: [PATCH 2/3] Standardize prompt generation pattern - move functions to dedicated files Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/cache_memory_prompt.go | 19 ++ pkg/workflow/cache_memory_prompt_test.go | 192 ++++++++++++++++++++ pkg/workflow/compiler_yaml.go | 24 --- pkg/workflow/pr.go | 56 ------ pkg/workflow/pr_prompt.go | 61 +++++++ pkg/workflow/pr_prompt_test.go | 215 +++++++++++++++++++++++ pkg/workflow/safe_outputs_prompt.go | 19 ++ pkg/workflow/safe_outputs_prompt_test.go | 190 ++++++++++++++++++++ 8 files changed, 696 insertions(+), 80 deletions(-) create mode 100644 pkg/workflow/cache_memory_prompt.go create mode 100644 pkg/workflow/cache_memory_prompt_test.go create mode 100644 pkg/workflow/pr_prompt.go create mode 100644 pkg/workflow/pr_prompt_test.go create mode 100644 pkg/workflow/safe_outputs_prompt.go create mode 100644 pkg/workflow/safe_outputs_prompt_test.go diff --git a/pkg/workflow/cache_memory_prompt.go b/pkg/workflow/cache_memory_prompt.go new file mode 100644 index 00000000000..cb9be4b69ed --- /dev/null +++ b/pkg/workflow/cache_memory_prompt.go @@ -0,0 +1,19 @@ +package workflow + +import ( + "strings" +) + +// generateCacheMemoryPromptStep generates a separate step for cache memory instructions +// when cache-memory is enabled, informing the agent about persistent storage capabilities +func (c *Compiler) generateCacheMemoryPromptStep(yaml *strings.Builder, config *CacheMemoryConfig) { + if config == nil || len(config.Caches) == 0 { + return + } + + appendPromptStepWithHeredoc(yaml, + "Append cache memory instructions to prompt", + func(y *strings.Builder) { + generateCacheMemoryPromptSection(y, config) + }) +} diff --git a/pkg/workflow/cache_memory_prompt_test.go b/pkg/workflow/cache_memory_prompt_test.go new file mode 100644 index 00000000000..8a49e979c36 --- /dev/null +++ b/pkg/workflow/cache_memory_prompt_test.go @@ -0,0 +1,192 @@ +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestCacheMemoryPromptIncludedWhenEnabled(t *testing.T) { + // Create a temporary directory for test files + tmpDir, err := os.MkdirTemp("", "gh-aw-cache-memory-prompt-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create a test workflow with cache-memory enabled + testFile := filepath.Join(tmpDir, "test-workflow.md") + testContent := `--- +on: push +engine: claude +tools: + cache-memory: true +--- + +# Test Workflow with Cache Memory + +This is a test workflow with cache-memory enabled. +` + + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatalf("Failed to create test workflow: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(testFile); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + lockStr := string(lockContent) + + // Test 1: Verify cache memory prompt step is created + if !strings.Contains(lockStr, "- name: Append cache memory instructions to prompt") { + t.Error("Expected 'Append cache memory instructions to prompt' step in generated workflow") + } + + // Test 2: Verify the instruction text contains cache folder information + if !strings.Contains(lockStr, "Cache Folder Available") { + t.Error("Expected 'Cache Folder Available' header in generated workflow") + } + + // Test 3: Verify the instruction text contains the cache directory path + if !strings.Contains(lockStr, "/tmp/gh-aw/cache-memory/") { + t.Error("Expected '/tmp/gh-aw/cache-memory/' reference in generated workflow") + } + + // Test 4: Verify the instruction mentions persistent cache + if !strings.Contains(lockStr, "persist") { + t.Error("Expected 'persist' reference in generated workflow") + } + + t.Logf("Successfully verified cache memory instructions are included in generated workflow") +} + +func TestCacheMemoryPromptNotIncludedWhenDisabled(t *testing.T) { + // Create a temporary directory for test files + tmpDir, err := os.MkdirTemp("", "gh-aw-no-cache-memory-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create a test workflow WITHOUT cache-memory + testFile := filepath.Join(tmpDir, "test-workflow.md") + testContent := `--- +on: push +engine: claude +tools: + github: +--- + +# Test Workflow without Cache Memory + +This is a test workflow without cache-memory. +` + + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatalf("Failed to create test workflow: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(testFile); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + lockStr := string(lockContent) + + // Test: Verify cache memory prompt step is NOT created + if strings.Contains(lockStr, "- name: Append cache memory instructions to prompt") { + t.Error("Did not expect 'Append cache memory instructions to prompt' step in workflow without cache-memory") + } + + if strings.Contains(lockStr, "Cache Folder Available") { + t.Error("Did not expect 'Cache Folder Available' header in workflow without cache-memory") + } + + t.Logf("Successfully verified cache memory instructions are NOT included when cache-memory is disabled") +} + +func TestCacheMemoryPromptMultipleCaches(t *testing.T) { + // Create a temporary directory for test files + tmpDir, err := os.MkdirTemp("", "gh-aw-multi-cache-memory-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create a test workflow with multiple cache-memory entries + testFile := filepath.Join(tmpDir, "test-workflow.md") + testContent := `--- +on: push +engine: claude +tools: + cache-memory: + - id: default + key: cache-1 + - id: session + key: cache-2 +--- + +# Test Workflow with Multiple Caches + +This is a test workflow with multiple cache-memory entries. +` + + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatalf("Failed to create test workflow: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(testFile); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + lockStr := string(lockContent) + + // Test 1: Verify cache memory prompt step is created + if !strings.Contains(lockStr, "- name: Append cache memory instructions to prompt") { + t.Error("Expected 'Append cache memory instructions to prompt' step in generated workflow") + } + + // Test 2: Verify plural form is used for multiple caches + if !strings.Contains(lockStr, "Cache Folders Available") { + t.Error("Expected 'Cache Folders Available' (plural) header for multiple caches") + } + + // Test 3: Verify both cache directories are mentioned + if !strings.Contains(lockStr, "/tmp/gh-aw/cache-memory/") { + t.Error("Expected '/tmp/gh-aw/cache-memory/' reference for default cache") + } + + if !strings.Contains(lockStr, "/tmp/gh-aw/cache-memory-session/") { + t.Error("Expected '/tmp/gh-aw/cache-memory-session/' reference for session cache") + } + + t.Logf("Successfully verified cache memory instructions handle multiple caches") +} diff --git a/pkg/workflow/compiler_yaml.go b/pkg/workflow/compiler_yaml.go index 1b3912d76e6..57e766ee41a 100644 --- a/pkg/workflow/compiler_yaml.go +++ b/pkg/workflow/compiler_yaml.go @@ -690,30 +690,6 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData) { WriteShellScriptToYAML(yaml, printPromptSummaryScript, " ") } -func (c *Compiler) generateCacheMemoryPromptStep(yaml *strings.Builder, config *CacheMemoryConfig) { - if config == nil || len(config.Caches) == 0 { - return - } - - appendPromptStepWithHeredoc(yaml, - "Append cache memory instructions to prompt", - func(y *strings.Builder) { - generateCacheMemoryPromptSection(y, config) - }) -} - -func (c *Compiler) generateSafeOutputsPromptStep(yaml *strings.Builder, safeOutputs *SafeOutputsConfig) { - if safeOutputs == nil { - return - } - - appendPromptStepWithHeredoc(yaml, - "Append safe outputs instructions to prompt", - func(y *strings.Builder) { - generateSafeOutputsPromptSection(y, safeOutputs) - }) -} - func (c *Compiler) generatePostSteps(yaml *strings.Builder, data *WorkflowData) { if data.PostSteps != "" { // Remove "post-steps:" line and adjust indentation, similar to CustomSteps processing diff --git a/pkg/workflow/pr.go b/pkg/workflow/pr.go index eb48a143b8d..6c5e4e5aa7c 100644 --- a/pkg/workflow/pr.go +++ b/pkg/workflow/pr.go @@ -5,62 +5,6 @@ import ( "strings" ) -// generatePRContextPromptStep generates a separate step for PR context instructions -func (c *Compiler) generatePRContextPromptStep(yaml *strings.Builder, data *WorkflowData) { - // Check if any of the workflow's event triggers are comment-related events - hasCommentTriggers := c.hasCommentRelatedTriggers(data) - - if !hasCommentTriggers { - return // No comment-related triggers, skip PR context instructions - } - - // Also check if checkout step will be added - only show prompt if checkout happens - needsCheckout := c.shouldAddCheckoutStep(data) - if !needsCheckout { - return // No checkout, so no PR branch checkout will happen - } - - // Check that permissions allow contents read access - permParser := NewPermissionsParser(data.Permissions) - if !permParser.HasContentsReadAccess() { - return // No contents read access, cannot checkout - } - - // Build the condition string - condition := BuildPRCommentCondition() - - // Use shared helper but we need to render condition manually since it requires RenderConditionAsIf - // which is more complex than a simple if: string - yaml.WriteString(" - name: Append PR context instructions to prompt\n") - RenderConditionAsIf(yaml, condition, " ") - yaml.WriteString(" env:\n") - yaml.WriteString(" GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt\n") - yaml.WriteString(" run: |\n") - WritePromptTextToYAML(yaml, prContextPromptText, " ") -} - -// hasCommentRelatedTriggers checks if the workflow has any comment-related event triggers -func (c *Compiler) hasCommentRelatedTriggers(data *WorkflowData) bool { - // Check for command trigger (which expands to comment events) - if data.Command != "" { - return true - } - - if data.On == "" { - return false - } - - // Check for comment-related event types in the "on" configuration - commentEvents := []string{"issue_comment", "pull_request_review_comment", "pull_request_review"} - for _, event := range commentEvents { - if strings.Contains(data.On, event) { - return true - } - } - - return false -} - // generatePRReadyForReviewCheckout generates a step to checkout the PR branch when PR context is available func (c *Compiler) generatePRReadyForReviewCheckout(yaml *strings.Builder, data *WorkflowData) { // Check that permissions allow contents read access diff --git a/pkg/workflow/pr_prompt.go b/pkg/workflow/pr_prompt.go new file mode 100644 index 00000000000..53adcc3566b --- /dev/null +++ b/pkg/workflow/pr_prompt.go @@ -0,0 +1,61 @@ +package workflow + +import ( + "strings" +) + +// generatePRContextPromptStep generates a separate step for PR context instructions +func (c *Compiler) generatePRContextPromptStep(yaml *strings.Builder, data *WorkflowData) { + // Check if any of the workflow's event triggers are comment-related events + hasCommentTriggers := c.hasCommentRelatedTriggers(data) + + if !hasCommentTriggers { + return // No comment-related triggers, skip PR context instructions + } + + // Also check if checkout step will be added - only show prompt if checkout happens + needsCheckout := c.shouldAddCheckoutStep(data) + if !needsCheckout { + return // No checkout, so no PR branch checkout will happen + } + + // Check that permissions allow contents read access + permParser := NewPermissionsParser(data.Permissions) + if !permParser.HasContentsReadAccess() { + return // No contents read access, cannot checkout + } + + // Build the condition string + condition := BuildPRCommentCondition() + + // Use shared helper but we need to render condition manually since it requires RenderConditionAsIf + // which is more complex than a simple if: string + yaml.WriteString(" - name: Append PR context instructions to prompt\n") + RenderConditionAsIf(yaml, condition, " ") + yaml.WriteString(" env:\n") + yaml.WriteString(" GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt\n") + yaml.WriteString(" run: |\n") + WritePromptTextToYAML(yaml, prContextPromptText, " ") +} + +// hasCommentRelatedTriggers checks if the workflow has any comment-related event triggers +func (c *Compiler) hasCommentRelatedTriggers(data *WorkflowData) bool { + // Check for command trigger (which expands to comment events) + if data.Command != "" { + return true + } + + if data.On == "" { + return false + } + + // Check for comment-related event types in the "on" configuration + commentEvents := []string{"issue_comment", "pull_request_review_comment", "pull_request_review"} + for _, event := range commentEvents { + if strings.Contains(data.On, event) { + return true + } + } + + return false +} diff --git a/pkg/workflow/pr_prompt_test.go b/pkg/workflow/pr_prompt_test.go new file mode 100644 index 00000000000..34512286b86 --- /dev/null +++ b/pkg/workflow/pr_prompt_test.go @@ -0,0 +1,215 @@ +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestPRContextPromptIncludedForIssueComment(t *testing.T) { + // Create a temporary directory for test files + tmpDir, err := os.MkdirTemp("", "gh-aw-pr-context-prompt-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create a test workflow with issue_comment trigger + testFile := filepath.Join(tmpDir, "test-workflow.md") + testContent := `--- +on: + issue_comment: + types: [created] +permissions: + contents: read +engine: claude +--- + +# Test Workflow with Issue Comment + +This is a test workflow with issue_comment trigger. +` + + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatalf("Failed to create test workflow: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(testFile); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + lockStr := string(lockContent) + + // Test 1: Verify PR context prompt step is created + if !strings.Contains(lockStr, "- name: Append PR context instructions to prompt") { + t.Error("Expected 'Append PR context instructions to prompt' step in generated workflow") + } + + // Test 2: Verify the instruction mentions PR branch checkout + if !strings.Contains(lockStr, "pull request") { + t.Error("Expected 'pull request' reference in generated workflow") + } + + t.Logf("Successfully verified PR context instructions are included for issue_comment trigger") +} + +func TestPRContextPromptIncludedForCommand(t *testing.T) { + // Create a temporary directory for test files + tmpDir, err := os.MkdirTemp("", "gh-aw-pr-context-command-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create a test workflow with command trigger + testFile := filepath.Join(tmpDir, "test-workflow.md") + testContent := `--- +on: + command: + name: mybot +permissions: + contents: read +engine: claude +--- + +# Test Workflow with Command + +This is a test workflow with command trigger. +` + + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatalf("Failed to create test workflow: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(testFile); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + lockStr := string(lockContent) + + // Test: Verify PR context prompt step is created for command triggers + if !strings.Contains(lockStr, "- name: Append PR context instructions to prompt") { + t.Error("Expected 'Append PR context instructions to prompt' step in workflow with command trigger") + } + + t.Logf("Successfully verified PR context instructions are included for command trigger") +} + +func TestPRContextPromptNotIncludedForPush(t *testing.T) { + // Create a temporary directory for test files + tmpDir, err := os.MkdirTemp("", "gh-aw-no-pr-context-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create a test workflow with push trigger (no comment triggers) + testFile := filepath.Join(tmpDir, "test-workflow.md") + testContent := `--- +on: push +permissions: + contents: read +engine: claude +--- + +# Test Workflow without Comment Triggers + +This is a test workflow with push trigger only. +` + + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatalf("Failed to create test workflow: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(testFile); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + lockStr := string(lockContent) + + // Test: Verify PR context prompt step is NOT created for push triggers + if strings.Contains(lockStr, "- name: Append PR context instructions to prompt") { + t.Error("Did not expect 'Append PR context instructions to prompt' step for push trigger") + } + + t.Logf("Successfully verified PR context instructions are NOT included for push trigger") +} + +func TestPRContextPromptNotIncludedWithoutCheckout(t *testing.T) { + // Create a temporary directory for test files + tmpDir, err := os.MkdirTemp("", "gh-aw-pr-no-checkout-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create a test workflow with comment trigger but no checkout (no contents permission) + testFile := filepath.Join(tmpDir, "test-workflow.md") + testContent := `--- +on: + issue_comment: + types: [created] +permissions: + issues: read +engine: claude +--- + +# Test Workflow without Contents Permission + +This is a test workflow without contents read permission. +` + + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatalf("Failed to create test workflow: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(testFile); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + lockStr := string(lockContent) + + // Test: Verify PR context prompt step is NOT created without contents permission + if strings.Contains(lockStr, "- name: Append PR context instructions to prompt") { + t.Error("Did not expect 'Append PR context instructions to prompt' step without contents read permission") + } + + t.Logf("Successfully verified PR context instructions are NOT included without contents permission") +} diff --git a/pkg/workflow/safe_outputs_prompt.go b/pkg/workflow/safe_outputs_prompt.go new file mode 100644 index 00000000000..e6ab80aa9ac --- /dev/null +++ b/pkg/workflow/safe_outputs_prompt.go @@ -0,0 +1,19 @@ +package workflow + +import ( + "strings" +) + +// generateSafeOutputsPromptStep generates a separate step for safe outputs instructions +// when safe-outputs are configured, informing the agent about available output capabilities +func (c *Compiler) generateSafeOutputsPromptStep(yaml *strings.Builder, safeOutputs *SafeOutputsConfig) { + if safeOutputs == nil { + return + } + + appendPromptStepWithHeredoc(yaml, + "Append safe outputs instructions to prompt", + func(y *strings.Builder) { + generateSafeOutputsPromptSection(y, safeOutputs) + }) +} diff --git a/pkg/workflow/safe_outputs_prompt_test.go b/pkg/workflow/safe_outputs_prompt_test.go new file mode 100644 index 00000000000..6fa9867db7d --- /dev/null +++ b/pkg/workflow/safe_outputs_prompt_test.go @@ -0,0 +1,190 @@ +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestSafeOutputsPromptIncludedWhenEnabled(t *testing.T) { + // Create a temporary directory for test files + tmpDir, err := os.MkdirTemp("", "gh-aw-safe-outputs-prompt-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create a test workflow with safe-outputs enabled + testFile := filepath.Join(tmpDir, "test-workflow.md") + testContent := `--- +on: issues +permissions: + contents: read + actions: read +engine: claude +safe-outputs: + create-issue: + labels: [automation] +--- + +# Test Workflow with Safe Outputs + +This is a test workflow with safe-outputs enabled. +` + + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatalf("Failed to create test workflow: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(testFile); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + lockStr := string(lockContent) + + // Test 1: Verify safe outputs prompt step is created + if !strings.Contains(lockStr, "- name: Append safe outputs instructions to prompt") { + t.Error("Expected 'Append safe outputs instructions to prompt' step in generated workflow") + } + + // Test 2: Verify the instruction text mentions creating issues + if !strings.Contains(lockStr, "Creating an Issue") { + t.Error("Expected 'Creating an Issue' reference in generated workflow") + } + + // Test 3: Verify the instruction text contains JSON output format + if !strings.Contains(lockStr, "create_issue") { + t.Error("Expected 'create_issue' output type reference in generated workflow") + } + + t.Logf("Successfully verified safe outputs instructions are included in generated workflow") +} + +func TestSafeOutputsPromptNotIncludedWhenDisabled(t *testing.T) { + // Create a temporary directory for test files + tmpDir, err := os.MkdirTemp("", "gh-aw-no-safe-outputs-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create a test workflow WITHOUT safe-outputs + testFile := filepath.Join(tmpDir, "test-workflow.md") + testContent := `--- +on: push +engine: claude +tools: + github: +--- + +# Test Workflow without Safe Outputs + +This is a test workflow without safe-outputs. +` + + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatalf("Failed to create test workflow: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(testFile); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + lockStr := string(lockContent) + + // Test: Verify safe outputs prompt step is NOT created + if strings.Contains(lockStr, "- name: Append safe outputs instructions to prompt") { + t.Error("Did not expect 'Append safe outputs instructions to prompt' step in workflow without safe-outputs") + } + + t.Logf("Successfully verified safe outputs instructions are NOT included when safe-outputs is disabled") +} + +func TestSafeOutputsPromptMultipleOutputTypes(t *testing.T) { + // Create a temporary directory for test files + tmpDir, err := os.MkdirTemp("", "gh-aw-multi-safe-outputs-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create a test workflow with multiple safe-output types + testFile := filepath.Join(tmpDir, "test-workflow.md") + testContent := `--- +on: issues +permissions: + contents: read + actions: read +engine: claude +safe-outputs: + create-issue: + labels: [automation] + add-comment: + max: 3 + create-pull-request: + draft: true +--- + +# Test Workflow with Multiple Safe Outputs + +This is a test workflow with multiple safe-output types. +` + + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatalf("Failed to create test workflow: %v", err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(testFile); err != nil { + t.Fatalf("Failed to compile workflow: %v", err) + } + + // Read the generated lock file + lockFile := strings.Replace(testFile, ".md", ".lock.yml", 1) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read generated lock file: %v", err) + } + + lockStr := string(lockContent) + + // Test 1: Verify safe outputs prompt step is created + if !strings.Contains(lockStr, "- name: Append safe outputs instructions to prompt") { + t.Error("Expected 'Append safe outputs instructions to prompt' step in generated workflow") + } + + // Test 2: Verify all output types are mentioned + if !strings.Contains(lockStr, "Creating an Issue") { + t.Error("Expected 'Creating an Issue' reference in generated workflow") + } + + if !strings.Contains(lockStr, "Adding a Comment") { + t.Error("Expected 'Adding a Comment' reference in generated workflow") + } + + if !strings.Contains(lockStr, "Creating a Pull Request") { + t.Error("Expected 'Creating a Pull Request' reference in generated workflow") + } + + t.Logf("Successfully verified safe outputs instructions handle multiple output types") +} From 33d5196cc750449f02777b9f0c51f36aa65067a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:10:28 +0000 Subject: [PATCH 3/3] Complete: Standardize prompt generation pattern organization Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../docs/reference/frontmatter-full.md | 53 ++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 035f3ca605f..f3dac260916 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -641,25 +641,38 @@ run-name: "example-value" jobs: {} -# Runner type for workflow execution (GitHub Actions standard field). Typically -# configured at the job level instead. +# Runner type for workflow execution (GitHub Actions standard field). Supports +# multiple forms: simple string for single runner label (e.g., 'ubuntu-latest'), +# array for runner selection with fallbacks, or object for GitHub-hosted runner +# groups with specific labels. For agentic workflows, runner selection matters +# when AI workloads require specific compute resources or when using self-hosted +# runners with specialized capabilities. Typically configured at the job level +# instead. See +# https://docs.github.com/en/actions/using-jobs/choosing-the-runner-for-a-job # (optional) # This field supports multiple formats (oneOf): -# Option 1: Runner type as string +# Option 1: Simple runner label string. Use for standard GitHub-hosted runners +# (e.g., 'ubuntu-latest', 'windows-latest', 'macos-latest') or self-hosted runner +# labels. Most common form for agentic workflows. runs-on: "example-value" -# Option 2: Runner type as array +# Option 2: Array of runner labels for selection with fallbacks. GitHub Actions +# will use the first available runner that matches any label in the array. Useful +# for high-availability setups or when multiple runner types are acceptable. runs-on: [] # Array items: string -# Option 3: Runner type as object +# Option 3: Runner group configuration for GitHub-hosted runners. Use this form to +# target specific runner groups (e.g., larger runners with more CPU/memory) or +# self-hosted runner pools with specific label requirements. Agentic workflows may +# benefit from larger runners for complex AI processing tasks. runs-on: - # Runner group name for self-hosted runners + # Runner group name for self-hosted runners or GitHub-hosted runner groups # (optional) group: "example-value" - # List of runner labels for self-hosted runners + # List of runner labels for self-hosted runners or GitHub-hosted runner selection # (optional) labels: [] # Array of strings @@ -671,22 +684,38 @@ runs-on: timeout-minutes: 1 # Concurrency control to limit concurrent workflow runs (GitHub Actions standard -# field). Agentic workflows use enhanced concurrency management. +# field). Supports two forms: simple string for basic group isolation, or object +# with cancel-in-progress option for advanced control. Agentic workflows enhance +# this with automatic per-engine concurrency policies (defaults to single job per +# engine across all workflows) and token-based rate limiting. Default behavior: +# workflows in the same group queue sequentially unless cancel-in-progress is +# true. See https://docs.github.com/en/actions/using-jobs/using-concurrency # (optional) # This field supports multiple formats (oneOf): -# Option 1: Simple concurrency group name to prevent multiple runs. Agentic -# workflows automatically generate enhanced concurrency policies. +# Option 1: Simple concurrency group name to prevent multiple runs in the same +# group. Use expressions like '${{ github.workflow }}' for per-workflow isolation +# or '${{ github.ref }}' for per-branch isolation. Agentic workflows automatically +# generate enhanced concurrency policies using 'gh-aw-{engine-id}' as the default +# group to limit concurrent AI workloads across all workflows using the same +# engine. concurrency: "example-value" # Option 2: Concurrency configuration object with group isolation and cancellation -# control +# control. Use object form when you need fine-grained control over whether to +# cancel in-progress runs. For agentic workflows, this is useful to prevent +# multiple AI agents from running simultaneously and consuming excessive resources +# or API quotas. concurrency: # Concurrency group name. Workflows in the same group cannot run simultaneously. + # Supports GitHub Actions expressions for dynamic group names based on branch, + # workflow, or other context. group: "example-value" # Whether to cancel in-progress workflows in the same concurrency group when a new - # one starts + # one starts. Default: false (queue new runs). Set to true for agentic workflows + # where only the latest run matters (e.g., PR analysis that becomes stale when new + # commits are pushed). # (optional) cancel-in-progress: true