Skip to content
Closed
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
65 changes: 65 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,77 @@ func (e EngineName) IsValid() bool {
return len(e) > 0
}

// FilePath represents a file system path.
// This semantic type distinguishes file paths from arbitrary strings,
// making path parameters explicit and enabling future validation logic.
//
// Example usage:
//
// const PromptFilePath FilePath = "/tmp/gh-aw/aw-prompts/prompt.txt"
// func ReadFile(path FilePath) ([]byte, error) { ... }
type FilePath string

// String returns the string representation of the file path
func (f FilePath) String() string {
return string(f)
}

// IsValid returns true if the file path is non-empty
func (f FilePath) IsValid() bool {
return len(f) > 0
}

// ByteSize represents a size in bytes.
// This semantic type distinguishes byte sizes from arbitrary integers,
// making size parameters explicit and preventing accidental misuse.
//
// Example usage:
//
// const DefaultMaxPatchSize ByteSize = 1024
// func AllocateBuffer(size ByteSize) []byte { ... }
type ByteSize int

// String returns the string representation of the byte size
func (b ByteSize) String() string {
return fmt.Sprintf("%d", b)
}

// IsValid returns true if the byte size is non-negative
func (b ByteSize) IsValid() bool {
return b >= 0
}

// MaxExpressionLineLength is the maximum length for a single line expression before breaking into multiline.
const MaxExpressionLineLength LineLength = 120

// ExpressionBreakThreshold is the threshold for breaking long lines at logical points.
const ExpressionBreakThreshold LineLength = 100

// File path constants

// PromptFilePath is the file path where workflow prompts are written during compilation.
// This path is used in the agent job to store the final prompt content before execution.
const PromptFilePath FilePath = "/tmp/gh-aw/aw-prompts/prompt.txt"

// Size constants

// DefaultMaxPatchSize is the default maximum size (in KB) for patches in safe outputs.
// This limit applies to create_pull_request and push_to_pull_request_branch operations.
const DefaultMaxPatchSize ByteSize = 1024

// DefaultYAMLBuilderSize is the default initial capacity for YAML string builder (in bytes).
// Allocating 256KB upfront minimizes reallocations during workflow generation.
// Average workflow generates ~200KB, so 256KB provides adequate buffer.
const DefaultYAMLBuilderSize ByteSize = 256 * 1024

// MaxChunkSize is the maximum size for content chunks (in bytes) when splitting prompts.
// Set to 20900 bytes to stay under GitHub Actions' 21000-byte limit with a 100-byte buffer.
const MaxChunkSize ByteSize = 20900

// ChunkSizeBuffer is the buffer size (in bytes) reserved when calculating chunk limits.
// This ensures content doesn't exceed GitHub Actions' size constraints.
const ChunkSizeBuffer ByteSize = 100

// DefaultMCPRegistryURL is the default MCP registry URL.
const DefaultMCPRegistryURL URL = "https://api.mcp.github.com/v0.1"

Expand Down
78 changes: 78 additions & 0 deletions pkg/constants/constants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,84 @@ func TestSemanticTypeAliases(t *testing.T) {
t.Errorf("CustomEngine = %q, want %q", custom, "custom")
}
})

// Test FilePath type
t.Run("FilePath type", func(t *testing.T) {
var testPath FilePath = "/tmp/test/file.txt"
if string(testPath) != "/tmp/test/file.txt" {
t.Errorf("FilePath conversion failed: got %q, want %q", testPath, "/tmp/test/file.txt")
}

// Test PromptFilePath has the correct type
promptPath := PromptFilePath
if string(promptPath) != "/tmp/gh-aw/aw-prompts/prompt.txt" {
t.Errorf("PromptFilePath = %q, want %q", promptPath, "/tmp/gh-aw/aw-prompts/prompt.txt")
}

// Test IsValid
if !testPath.IsValid() {
t.Error("FilePath should be valid when non-empty")
}

var emptyPath FilePath = ""
if emptyPath.IsValid() {
t.Error("FilePath should be invalid when empty")
}

// Test String method
if testPath.String() != "/tmp/test/file.txt" {
t.Errorf("FilePath.String() = %q, want %q", testPath.String(), "/tmp/test/file.txt")
}
})

// Test ByteSize type
t.Run("ByteSize type", func(t *testing.T) {
var testSize ByteSize = 1024
if int(testSize) != 1024 {
t.Errorf("ByteSize conversion failed: got %d, want %d", testSize, 1024)
}

// Test size constants have the correct type
maxPatchSize := DefaultMaxPatchSize
if int(maxPatchSize) != 1024 {
t.Errorf("DefaultMaxPatchSize = %d, want %d", maxPatchSize, 1024)
}

yamlBuilderSize := DefaultYAMLBuilderSize
if int(yamlBuilderSize) != 256*1024 {
t.Errorf("DefaultYAMLBuilderSize = %d, want %d", yamlBuilderSize, 256*1024)
}

maxChunkSize := MaxChunkSize
if int(maxChunkSize) != 20900 {
t.Errorf("MaxChunkSize = %d, want %d", maxChunkSize, 20900)
}

chunkBuffer := ChunkSizeBuffer
if int(chunkBuffer) != 100 {
t.Errorf("ChunkSizeBuffer = %d, want %d", chunkBuffer, 100)
}

// Test IsValid
if !testSize.IsValid() {
t.Error("ByteSize should be valid when non-negative")
}

var zeroSize ByteSize = 0
if !zeroSize.IsValid() {
t.Error("ByteSize should be valid when zero")
}

var negativeSize ByteSize = -1
if negativeSize.IsValid() {
t.Error("ByteSize should be invalid when negative")
}

// Test String method
if testSize.String() != "1024" {
t.Errorf("ByteSize.String() = %q, want %q", testSize.String(), "1024")
}
})
}

func TestTypeSafetyBetweenSemanticTypes(t *testing.T) {
Expand Down
5 changes: 3 additions & 2 deletions pkg/workflow/compiler_safe_outputs_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"

"github.com/githubnext/gh-aw/pkg/constants"
"github.com/githubnext/gh-aw/pkg/logger"
)

Expand Down Expand Up @@ -295,7 +296,7 @@ var handlerRegistry = map[string]handlerBuilder{
return nil
}
c := cfg.CreatePullRequests
maxPatchSize := 1024 // default 1024 KB
maxPatchSize := int(constants.DefaultMaxPatchSize) // default 1024 KB
if cfg.MaximumPatchSize > 0 {
maxPatchSize = cfg.MaximumPatchSize
}
Expand All @@ -319,7 +320,7 @@ var handlerRegistry = map[string]handlerBuilder{
return nil
}
c := cfg.PushToPullRequestBranch
maxPatchSize := 1024 // default 1024 KB
maxPatchSize := int(constants.DefaultMaxPatchSize) // default 1024 KB
if cfg.MaximumPatchSize > 0 {
maxPatchSize = cfg.MaximumPatchSize
}
Expand Down
10 changes: 5 additions & 5 deletions pkg/workflow/compiler_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func (c *Compiler) generateYAML(data *WorkflowData, markdownPath string) (string
// Pre-allocate builder capacity based on estimated workflow size
// Average workflow generates ~200KB, allocate 256KB to minimize reallocations
var yaml strings.Builder
yaml.Grow(256 * 1024)
yaml.Grow(int(constants.DefaultYAMLBuilderSize))

// Generate workflow header comments (including hash)
c.generateWorkflowHeader(&yaml, data, frontmatterHash)
Expand All @@ -198,8 +198,8 @@ func (c *Compiler) generateYAML(data *WorkflowData, markdownPath string) (string
}

func splitContentIntoChunks(content string) []string {
const maxChunkSize = 20900 // 21000 - 100 character buffer
const indentSpaces = " " // 10 spaces added to each line
const maxChunkSize = int(constants.MaxChunkSize) // 21000 - 100 character buffer
const indentSpaces = " " // 10 spaces added to each line

lines := strings.Split(content, "\n")
var chunks []string
Expand Down Expand Up @@ -278,13 +278,13 @@ func (c *Compiler) generatePrompt(yaml *strings.Builder, data *WorkflowData) {
// Validate that all placeholders have been substituted
yaml.WriteString(" - name: Validate prompt placeholders\n")
yaml.WriteString(" env:\n")
yaml.WriteString(" GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt\n")
fmt.Fprintf(yaml, " GH_AW_PROMPT: %s\n", constants.PromptFilePath)
yaml.WriteString(" run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh\n")

// Print prompt (merged into prompt generation)
yaml.WriteString(" - name: Print prompt\n")
yaml.WriteString(" env:\n")
yaml.WriteString(" GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt\n")
fmt.Fprintf(yaml, " GH_AW_PROMPT: %s\n", constants.PromptFilePath)
yaml.WriteString(" run: bash /opt/gh-aw/actions/print_prompt_summary.sh\n")
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/create_pull_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (c *Compiler) buildCreateOutputPullRequestJob(data *WorkflowData, mainJobNa
customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_PR_AUTO_MERGE: %q\n", fmt.Sprintf("%t", data.SafeOutputs.CreatePullRequests.AutoMerge)))

// Pass the maximum patch size configuration
maxPatchSize := 1024 // Default value
maxPatchSize := int(constants.DefaultMaxPatchSize) // Default value
if data.SafeOutputs != nil && data.SafeOutputs.MaximumPatchSize > 0 {
maxPatchSize = data.SafeOutputs.MaximumPatchSize
}
Expand Down