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
53 changes: 41 additions & 12 deletions docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
19 changes: 19 additions & 0 deletions pkg/workflow/cache_memory_prompt.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
192 changes: 192 additions & 0 deletions pkg/workflow/cache_memory_prompt_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
24 changes: 0 additions & 24 deletions pkg/workflow/compiler_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 0 additions & 56 deletions pkg/workflow/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading