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
5 changes: 5 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6689,6 +6689,11 @@
"action-version": {
"type": "string",
"description": "Version of the setup action to use (e.g., 'v4', 'v5'). Overrides the default action version."
},
"if": {
"type": "string",
"description": "Optional GitHub Actions if condition to control when the runtime setup step runs. Supports standard GitHub Actions expression syntax. Useful for conditionally installing runtimes based on file presence (e.g., \"hashFiles('go.mod') != ''\" to install Go only when go.mod exists).",
"examples": ["hashFiles('go.mod') != ''", "hashFiles('package.json') != ''", "hashFiles('requirements.txt') != '' || hashFiles('pyproject.toml') != ''", "hashFiles('uv.lock') != ''", "github.event_name == 'workflow_dispatch'"]
}
},
"additionalProperties": false
Expand Down
113 changes: 89 additions & 24 deletions pkg/workflow/frontmatter_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var frontmatterTypesLog = logger.New("workflow:frontmatter_types")
// RuntimeConfig represents the configuration for a single runtime
type RuntimeConfig struct {
Version string `json:"version,omitempty"` // Version of the runtime (e.g., "20" for Node, "3.11" for Python)
If string `json:"if,omitempty"` // Optional GitHub Actions if condition (e.g., "hashFiles('go.mod') != ''")
}

// RuntimesConfig represents the configuration for all runtime environments
Expand Down Expand Up @@ -261,29 +262,39 @@ func parseRuntimesConfig(runtimes map[string]any) (*RuntimesConfig, error) {
continue
}

versionAny, hasVersion := configMap["version"]
if !hasVersion {
continue
// Extract version (optional)
var version string
if versionAny, hasVersion := configMap["version"]; hasVersion {
Comment on lines +265 to +267
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The comment "Extract version (optional)" is misleading. While the version field itself is optional in the YAML, the changed logic now allows runtime configs with only an if condition and no version. Consider updating the comment to clarify that a runtime config can have either a version, an if condition, or both.

Copilot uses AI. Check for mistakes.
// Convert version to string
switch v := versionAny.(type) {
case string:
version = v
case int:
version = fmt.Sprintf("%d", v)
case float64:
if v == float64(int(v)) {
version = fmt.Sprintf("%d", int(v))
} else {
version = fmt.Sprintf("%g", v)
}
default:
continue
}
}

// Convert version to string
var version string
switch v := versionAny.(type) {
case string:
version = v
case int:
version = fmt.Sprintf("%d", v)
case float64:
if v == float64(int(v)) {
version = fmt.Sprintf("%d", int(v))
} else {
version = fmt.Sprintf("%g", v)
// Extract if condition (optional)
var ifCondition string
if ifAny, hasIf := configMap["if"]; hasIf {
if ifStr, ok := ifAny.(string); ok {
ifCondition = ifStr
}
default:
continue
}

runtimeConfig := &RuntimeConfig{Version: version}
// Create runtime config with both version and if condition
runtimeConfig := &RuntimeConfig{
Version: version,
If: ifCondition,
}

// Map to specific runtime field
switch runtimeID {
Expand Down Expand Up @@ -675,22 +686,76 @@ func runtimesConfigToMap(config *RuntimesConfig) map[string]any {
result := make(map[string]any)

if config.Node != nil {
result["node"] = map[string]any{"version": config.Node.Version}
nodeMap := map[string]any{}
if config.Node.Version != "" {
nodeMap["version"] = config.Node.Version
}
if config.Node.If != "" {
nodeMap["if"] = config.Node.If
}
if len(nodeMap) > 0 {
result["node"] = nodeMap
}
Comment on lines +689 to +698
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The conversion logic for each runtime (node, python, go, uv, bun, deno) is duplicated six times with only the runtime name and config field changing. This repetitive code could be refactored into a helper function that takes the runtime name and config as parameters, reducing code duplication and making future modifications easier.

Copilot uses AI. Check for mistakes.
}
if config.Python != nil {
result["python"] = map[string]any{"version": config.Python.Version}
pythonMap := map[string]any{}
if config.Python.Version != "" {
pythonMap["version"] = config.Python.Version
}
if config.Python.If != "" {
pythonMap["if"] = config.Python.If
}
if len(pythonMap) > 0 {
result["python"] = pythonMap
}
}
if config.Go != nil {
result["go"] = map[string]any{"version": config.Go.Version}
goMap := map[string]any{}
if config.Go.Version != "" {
goMap["version"] = config.Go.Version
}
if config.Go.If != "" {
goMap["if"] = config.Go.If
}
if len(goMap) > 0 {
result["go"] = goMap
}
}
if config.UV != nil {
result["uv"] = map[string]any{"version": config.UV.Version}
uvMap := map[string]any{}
if config.UV.Version != "" {
uvMap["version"] = config.UV.Version
}
if config.UV.If != "" {
uvMap["if"] = config.UV.If
}
if len(uvMap) > 0 {
result["uv"] = uvMap
}
}
if config.Bun != nil {
result["bun"] = map[string]any{"version": config.Bun.Version}
bunMap := map[string]any{}
if config.Bun.Version != "" {
bunMap["version"] = config.Bun.Version
}
if config.Bun.If != "" {
bunMap["if"] = config.Bun.If
}
if len(bunMap) > 0 {
result["bun"] = bunMap
}
}
if config.Deno != nil {
result["deno"] = map[string]any{"version": config.Deno.Version}
denoMap := map[string]any{}
if config.Deno.Version != "" {
denoMap["version"] = config.Deno.Version
}
if config.Deno.If != "" {
denoMap["if"] = config.Deno.If
}
if len(denoMap) > 0 {
result["deno"] = denoMap
}
}

if len(result) == 0 {
Expand Down
177 changes: 177 additions & 0 deletions pkg/workflow/frontmatter_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1332,3 +1332,180 @@ func TestTypedConfigsBackwardCompatibility(t *testing.T) {
}
})
}

func TestParseRuntimesConfigWithIfCondition(t *testing.T) {
tests := []struct {
name string
runtimes map[string]any
expected map[string]RuntimeConfig
}{
{
name: "runtime with if condition",
runtimes: map[string]any{
"go": map[string]any{
"version": "1.25",
"if": "hashFiles('go.mod') != ''",
},
},
expected: map[string]RuntimeConfig{
"go": {
Version: "1.25",
If: "hashFiles('go.mod') != ''",
},
},
},
{
name: "runtime with only if condition",
runtimes: map[string]any{
"uv": map[string]any{
"if": "hashFiles('uv.lock') != ''",
},
},
expected: map[string]RuntimeConfig{
"uv": {
Version: "",
If: "hashFiles('uv.lock') != ''",
},
},
},
{
name: "multiple runtimes with if conditions",
runtimes: map[string]any{
"go": map[string]any{
"version": "1.25",
"if": "hashFiles('go.mod') != ''",
},
"python": map[string]any{
"version": "3.11",
"if": "hashFiles('requirements.txt') != ''",
},
"node": map[string]any{
"version": "20",
"if": "hashFiles('package.json') != ''",
},
},
expected: map[string]RuntimeConfig{
"go": {
Version: "1.25",
If: "hashFiles('go.mod') != ''",
},
"python": {
Version: "3.11",
If: "hashFiles('requirements.txt') != ''",
},
"node": {
Version: "20",
If: "hashFiles('package.json') != ''",
},
},
},
{
name: "runtime without if condition",
runtimes: map[string]any{
"node": map[string]any{
"version": "20",
},
},
expected: map[string]RuntimeConfig{
"node": {
Version: "20",
If: "",
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config, err := parseRuntimesConfig(tt.runtimes)
if err != nil {
t.Fatalf("parseRuntimesConfig failed: %v", err)
}

for runtimeID, expectedConfig := range tt.expected {
var actualConfig *RuntimeConfig
switch runtimeID {
case "node":
actualConfig = config.Node
case "python":
actualConfig = config.Python
case "go":
actualConfig = config.Go
case "uv":
actualConfig = config.UV
case "bun":
actualConfig = config.Bun
case "deno":
actualConfig = config.Deno
}

if actualConfig == nil {
t.Errorf("Runtime %s not found in config", runtimeID)
continue
}

if actualConfig.Version != expectedConfig.Version {
t.Errorf("Runtime %s version: got %q, want %q", runtimeID, actualConfig.Version, expectedConfig.Version)
}

if actualConfig.If != expectedConfig.If {
t.Errorf("Runtime %s if condition: got %q, want %q", runtimeID, actualConfig.If, expectedConfig.If)
}
}
})
}
}

func TestRuntimesConfigToMapWithIfCondition(t *testing.T) {
config := &RuntimesConfig{
Go: &RuntimeConfig{
Version: "1.25",
If: "hashFiles('go.mod') != ''",
},
Python: &RuntimeConfig{
Version: "3.11",
If: "hashFiles('requirements.txt') != ''",
},
Node: &RuntimeConfig{
Version: "20",
},
}

result := runtimesConfigToMap(config)

// Check Go runtime
goMap, ok := result["go"].(map[string]any)
if !ok {
t.Fatal("go runtime not found in result")
}
if goMap["version"] != "1.25" {
t.Errorf("go version: got %v, want 1.25", goMap["version"])
}
if goMap["if"] != "hashFiles('go.mod') != ''" {
t.Errorf("go if condition: got %v, want hashFiles('go.mod') != ''", goMap["if"])
}

// Check Python runtime
pythonMap, ok := result["python"].(map[string]any)
if !ok {
t.Fatal("python runtime not found in result")
}
if pythonMap["version"] != "3.11" {
t.Errorf("python version: got %v, want 3.11", pythonMap["version"])
}
if pythonMap["if"] != "hashFiles('requirements.txt') != ''" {
t.Errorf("python if condition: got %v, want hashFiles('requirements.txt') != ''", pythonMap["if"])
}

// Check Node runtime (no if condition)
nodeMap, ok := result["node"].(map[string]any)
if !ok {
t.Fatal("node runtime not found in result")
}
if nodeMap["version"] != "20" {
t.Errorf("node version: got %v, want 20", nodeMap["version"])
}
if _, hasIf := nodeMap["if"]; hasIf {
t.Error("node should not have if condition in map")
}
}
1 change: 1 addition & 0 deletions pkg/workflow/runtime_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type RuntimeRequirement struct {
Version string // Empty string means use default
ExtraFields map[string]any // Additional 'with' fields from user's setup step (e.g., cache settings)
GoModFile string // Path to go.mod file for Go runtime (Go-specific)
IfCondition string // Optional GitHub Actions if condition
}

// knownRuntimes is the list of all supported runtime configurations (alphabetically sorted by ID)
Expand Down
Loading
Loading