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
7 changes: 3 additions & 4 deletions pkg/cli/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ var (
)

func init() {
// Set the default version in the workflow package
// This allows workflow.NewCompiler() to auto-detect the version
workflow.SetDefaultVersion(version)
// Set the version in the workflow package so NewCompiler() auto-detects it
workflow.SetVersion(version)
}

// SetVersionInfo sets the version information for the CLI and workflow package
func SetVersionInfo(v string) {
version = v
workflow.SetDefaultVersion(v) // Keep workflow package in sync
workflow.SetVersion(v) // Keep workflow package in sync
}

// GetVersion returns the current version
Expand Down
2 changes: 0 additions & 2 deletions pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -699,8 +699,6 @@ func (c *Compiler) CompileWorkflowData(workflowData *WorkflowData, markdownPath

// generatePostSteps generates the post-steps section that runs after AI execution

// convertStepToYAML converts a step map to YAML string with proper indentation

// generateEngineExecutionSteps uses the new GetExecutionSteps interface method

// generateAgentVersionCapture generates a step that captures the agent version if the engine supports it
Expand Down
4 changes: 1 addition & 3 deletions pkg/workflow/compiler_generation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import (

// Test convertStepToYAML function
func TestConvertStepToYAML(t *testing.T) {
compiler := NewCompiler()

tests := []struct {
name string
stepMap map[string]any
Expand Down Expand Up @@ -97,7 +95,7 @@ func TestConvertStepToYAML(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := compiler.convertStepToYAML(tt.stepMap)
result, err := ConvertStepToYAML(tt.stepMap)

if tt.hasError {
if err == nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/compiler_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,7 @@ func (c *Compiler) buildCustomJobs(data *WorkflowData, activationJobCreated bool
pinnedStep := ApplyActionPinToTypedStep(typedStep, data)

// Convert back to map for YAML generation
stepYAML, err := c.convertStepToYAML(pinnedStep.ToMap())
stepYAML, err := ConvertStepToYAML(pinnedStep.ToMap())
if err != nil {
return fmt.Errorf("failed to convert step to YAML for job '%s': %w", jobName, err)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/workflow/compiler_pre_activation_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ func (c *Compiler) buildPreActivationJob(data *WorkflowData, needsPermissionChec
if len(data.OnSteps) > 0 {
compilerActivationJobsLog.Printf("Adding %d on.steps to pre-activation job", len(data.OnSteps))
for i, stepMap := range data.OnSteps {
stepYAML, err := c.convertStepToYAML(stepMap)
stepYAML, err := ConvertStepToYAML(stepMap)
if err != nil {
return nil, fmt.Errorf("failed to convert on.steps[%d] to YAML: %w", i, err)
}
Expand Down Expand Up @@ -486,7 +486,7 @@ func (c *Compiler) extractPreActivationCustomFields(jobs map[string]any) ([]stri
}

// Convert step to YAML
stepYAML, err := c.convertStepToYAML(stepMap)
stepYAML, err := ConvertStepToYAML(stepMap)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert jobs.%s.steps[%d] to YAML: %w", jobName, i, err)
}
Expand Down
12 changes: 0 additions & 12 deletions pkg/workflow/compiler_safe_output_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package workflow

import (
"fmt"
"strings"

"github.com/github/gh-aw/pkg/logger"
"github.com/github/gh-aw/pkg/stringutil"
)

var compilerSafeOutputJobsLog = logger.New("workflow:compiler_safe_output_jobs")
Expand Down Expand Up @@ -266,13 +264,3 @@ func (c *Compiler) buildCallWorkflowJobs(data *WorkflowData, markdownPath string

return jobNames, nil
}

// sanitizeJobName converts a workflow name to a valid GitHub Actions job name.
// It delegates normalization to NormalizeSafeOutputIdentifier (which converts
// hyphens to underscores), then converts underscores back to hyphens for
// GitHub Actions job name conventions.
func sanitizeJobName(workflowName string) string {
normalized := stringutil.NormalizeSafeOutputIdentifier(workflowName)
// NormalizeSafeOutputIdentifier uses underscores; convert to hyphens for job names
return strings.ReplaceAll(normalized, "_", "-")
}
2 changes: 1 addition & 1 deletion pkg/workflow/compiler_safe_outputs_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa
typedStep.Env["GH_HOST"] = "${{ steps.ghes-host-config.outputs.GH_HOST }}"
}
pinnedStep := ApplyActionPinToTypedStep(typedStep, data)
stepYAML, err := c.convertStepToYAML(pinnedStep.ToMap())
stepYAML, err := ConvertStepToYAML(pinnedStep.ToMap())
if err != nil {
return nil, nil, fmt.Errorf("failed to convert safe-outputs step at index %d to YAML: %w", i, err)
}
Expand Down
14 changes: 2 additions & 12 deletions pkg/workflow/compiler_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,6 @@ type FileTracker interface {
TrackCreated(filePath string)
}

// defaultVersion holds the version string for compiler creation
// This is set by the CLI package during initialization
var defaultVersion = "dev"

// SetDefaultVersion sets the default version for compiler creation
// This should be called once during CLI initialization
func SetDefaultVersion(version string) {
defaultVersion = version
}

// Compiler handles converting markdown workflows to GitHub Actions YAML
type Compiler struct {
verbose bool
Expand Down Expand Up @@ -107,8 +97,8 @@ type Compiler struct {
// By default, it auto-detects the version and action mode.
// Common options: WithVerbose, WithEngineOverride, WithNoEmit, WithSkipValidation
func NewCompiler(opts ...CompilerOption) *Compiler {
// Get default version
version := defaultVersion
// Get the current compiler version (set by SetVersion during CLI initialization)
version := GetVersion()

// Auto-detect git repository root for action cache path resolution
// This ensures actions-lock.json is created at repo root regardless of CWD
Expand Down
16 changes: 0 additions & 16 deletions pkg/workflow/compiler_yaml_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,6 @@ func GetWorkflowIDFromPath(markdownPath string) string {
return strings.TrimSuffix(filepath.Base(markdownPath), ".md")
}

// SanitizeWorkflowIDForCacheKey sanitizes a workflow ID for use in cache keys.
// It removes all hyphens and converts to lowercase to create a filesystem-safe identifier.
// Example: "Smoke-Copilot" -> "smokecopilot"
func SanitizeWorkflowIDForCacheKey(workflowID string) string {
// Convert to lowercase
sanitized := strings.ToLower(workflowID)
// Remove all hyphens
sanitized = strings.ReplaceAll(sanitized, "-", "")
return sanitized
}

// ConvertStepToYAML converts a step map to YAML string with proper indentation.
// This is a shared utility function used by all engines and the compiler.
func ConvertStepToYAML(stepMap map[string]any) (string, error) {
Expand Down Expand Up @@ -90,11 +79,6 @@ func ConvertStepToYAML(stepMap map[string]any) (string, error) {
return result.String(), nil
}

// convertStepToYAML is the method form of ConvertStepToYAML for callers that hold a *Compiler.
func (c *Compiler) convertStepToYAML(stepMap map[string]any) (string, error) {
return ConvertStepToYAML(stepMap)
}

// unquoteUsesWithComments removes quotes from uses values that contain version comments.
// Transforms: uses: "slug@sha # v1" -> uses: slug@sha # v1
// This is needed because the YAML marshaller quotes strings containing #, but GitHub Actions
Expand Down
11 changes: 0 additions & 11 deletions pkg/workflow/compiler_yaml_main_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -772,17 +772,6 @@ func (c *Compiler) generateLegacyAgentImportCheckout(yaml *strings.Builder, agen
compilerYamlLog.Printf("Added legacy agent checkout step: %s/%s@%s -> %s", owner, repo, ref, checkoutPath)
}

// sanitizeRefForPath sanitizes a git ref for use in a file path
// Replaces characters that are problematic in file paths with safe alternatives
func sanitizeRefForPath(ref string) string {
// Replace slashes with dashes (for refs like "feature/my-branch")
sanitized := strings.ReplaceAll(ref, "/", "-")
// Replace other problematic characters
sanitized = strings.ReplaceAll(sanitized, ":", "-")
sanitized = strings.ReplaceAll(sanitized, "\\", "-")
return sanitized
}

// generateDevModeCLIBuildSteps generates the steps needed to build the gh-aw CLI and Docker image in dev mode
// These steps are injected after checkout in dev mode to create a locally built Docker image that includes
// the gh-aw binary and all dependencies. The agentic-workflows MCP server uses this image instead of alpine:latest.
Expand Down
4 changes: 1 addition & 3 deletions pkg/workflow/multiline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import (
// TestMultilineStringHandling tests that multiline strings in with parameters
// are correctly serialized with proper YAML indentation
func TestMultilineStringHandling(t *testing.T) {
compiler := NewCompiler()

testCases := []struct {
name string
stepMap map[string]any
Expand Down Expand Up @@ -92,7 +90,7 @@ echo "Build complete"`,

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
result, err := compiler.convertStepToYAML(tt.stepMap)
result, err := ConvertStepToYAML(tt.stepMap)
if err != nil {
t.Fatalf("convertStepToYAML failed: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/safe_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func (c *Compiler) buildSafeJobs(data *WorkflowData, threatDetectionEnabled bool
pinnedStep := ApplyActionPinToTypedStep(typedStep, data)

// Convert back to map for YAML generation
stepYAML, err := c.convertStepToYAML(pinnedStep.ToMap())
stepYAML, err := ConvertStepToYAML(pinnedStep.ToMap())
if err != nil {
return nil, fmt.Errorf("failed to convert step to YAML for safe job %s: %w", jobName, err)
}
Expand Down
36 changes: 36 additions & 0 deletions pkg/workflow/strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
// Functions:
// - SanitizeName: Configurable sanitization with character preservation options
// - SanitizeWorkflowName: Sanitizes for artifact names and file paths (preserves dots, underscores)
// - SanitizeWorkflowIDForCacheKey: Sanitizes workflow ID for use in cache keys (removes hyphens)
// - sanitizeJobName: Sanitizes workflow name to a valid GitHub Actions job name
// - sanitizeRefForPath: Sanitizes a git ref for use in a file path
// - SanitizeIdentifier (workflow_name.go): Creates clean identifiers for user agents
//
// Example:
Expand Down Expand Up @@ -82,6 +85,7 @@ import (
"strings"

"github.com/github/gh-aw/pkg/logger"
"github.com/github/gh-aw/pkg/stringutil"
)

var stringsLog = logger.New("workflow:strings")
Expand Down Expand Up @@ -311,6 +315,38 @@ func PrettifyToolName(toolName string) string {
return toolName
}

// SanitizeWorkflowIDForCacheKey sanitizes a workflow ID for use in cache keys.
// It removes all hyphens and converts to lowercase to create a filesystem-safe identifier.
// Example: "Smoke-Copilot" -> "smokecopilot"
func SanitizeWorkflowIDForCacheKey(workflowID string) string {
// Convert to lowercase
sanitized := strings.ToLower(workflowID)
// Remove all hyphens
sanitized = strings.ReplaceAll(sanitized, "-", "")
return sanitized
}

// sanitizeJobName converts a workflow name to a valid GitHub Actions job name.
// It delegates normalization to NormalizeSafeOutputIdentifier (which converts
// hyphens to underscores), then converts underscores back to hyphens for
// GitHub Actions job name conventions.
func sanitizeJobName(workflowName string) string {
normalized := stringutil.NormalizeSafeOutputIdentifier(workflowName)
// NormalizeSafeOutputIdentifier uses underscores; convert to hyphens for job names
return strings.ReplaceAll(normalized, "_", "-")
}

// sanitizeRefForPath sanitizes a git ref for use in a file path.
// Replaces characters that are problematic in file paths with safe alternatives.
func sanitizeRefForPath(ref string) string {
// Replace slashes with dashes (for refs like "feature/my-branch")
sanitized := strings.ReplaceAll(ref, "/", "-")
// Replace other problematic characters
sanitized = strings.ReplaceAll(sanitized, ":", "-")
sanitized = strings.ReplaceAll(sanitized, "\\", "-")
return sanitized
}

// formatList formats a list of strings as a comma-separated list with natural language conjunction
func formatList(items []string) string {
if len(items) == 0 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/threat_detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ func (c *Compiler) buildCustomThreatDetectionSteps(steps []any) []string {
var result []string
for _, step := range steps {
if stepMap, ok := step.(map[string]any); ok {
if stepYAML, err := c.convertStepToYAML(stepMap); err == nil {
if stepYAML, err := ConvertStepToYAML(stepMap); err == nil {
result = append(result, stepYAML)
}
}
Expand Down
16 changes: 12 additions & 4 deletions pkg/workflow/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,25 @@ import (

var versionLog = logger.New("workflow:version")

// compilerVersion holds the version of the compiler, set at runtime.
// This is used to include version information in generated workflow headers.
// compilerVersion is the single source of truth for the compiler version.
// It is set at runtime by SetVersion (called during CLI initialization) and used:
// - In generated workflow headers (via GetVersion)
// - When creating new Compiler instances (NewCompiler reads it via GetVersion)
//
// Initialization flow:
//
// main.go → cli.SetVersionInfo(v) → workflow.SetVersion(v) (sets compilerVersion)
// → NewCompiler() (reads compilerVersion via GetVersion)
Comment on lines +14 to +17
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documented initialization flow here doesn’t match the actual call chain: pkg/cli/version.go calls workflow.SetVersion in init(), and cmd/gh-aw/main.go calls workflow.SetVersion(version) even after cli.SetVersionInfo(version) (which itself calls workflow.SetVersion). Please update this comment to reflect the real flow (or simplify the flow in code) to avoid future confusion.

This issue also appears on line 25 of the same file.

Copilot uses AI. Check for mistakes.
var compilerVersion = "dev"

// isReleaseBuild indicates whether this binary was built as a release.
// This is set at build time via -X linker flag and used to determine
// if version information should be included in generated workflows.
var isReleaseBuild = false

// SetVersion sets the compiler version for inclusion in generated workflow headers.
// Only non-dev versions are included in the generated headers.
// SetVersion sets the compiler version. Call once during CLI initialization.
// The version is used in generated workflow headers and as the default version
// for new Compiler instances created via NewCompiler.
func SetVersion(v string) {
versionLog.Printf("Setting compiler version: %s", v)
compilerVersion = v
Expand Down
Loading