From e609ed73911a0d009bd4e50b587ca11f8fdc2a79 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Sat, 14 Mar 2026 19:01:02 +0000
Subject: [PATCH] refactor: simplify lock file reading and project config
 parsing

- Extract readLockFile() helper in run_workflow_validation.go to
  eliminate duplicated file reading/parsing logic between IsRunnable
  and getWorkflowInputs (~30 lines removed)
- Use existing extractStringFromMap() helper in project_config_parsing.go
  to replace verbose 4-line string extraction pattern with single-line
  struct initialization (~27 lines removed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 pkg/cli/run_workflow_validation.go     | 70 ++++++++------------------
 pkg/workflow/project_config_parsing.go | 51 ++++---------------
 2 files changed, 32 insertions(+), 89 deletions(-)

diff --git a/pkg/cli/run_workflow_validation.go b/pkg/cli/run_workflow_validation.go
index b768065d8b9..d4dc7bf1f78 100644
--- a/pkg/cli/run_workflow_validation.go
+++ b/pkg/cli/run_workflow_validation.go
@@ -28,54 +28,54 @@ func getLockFilePath(markdownPath string) string {
 	return strings.TrimSuffix(markdownPath, ".md") + ".lock.yml"
 }
 
-// IsRunnable checks if a workflow can be run (has schedule or workflow_dispatch trigger)
-// This function checks the compiled .lock.yml file because that's what GitHub Actions uses.
-func IsRunnable(markdownPath string) (bool, error) {
-	// Convert markdown path to lock file path
+// readLockFile reads and parses a workflow's compiled lock file.
+func readLockFile(markdownPath string) (map[string]any, error) {
 	lockPath := getLockFilePath(markdownPath)
 	cleanLockPath := filepath.Clean(lockPath)
 
-	validationLog.Printf("Checking if workflow is runnable: markdown=%s, lock=%s", markdownPath, lockPath)
-
-	// Check if the lock file exists
 	if _, err := os.Stat(cleanLockPath); os.IsNotExist(err) {
 		validationLog.Printf("Lock file does not exist: %s", cleanLockPath)
-		return false, errors.New("workflow has not been compiled yet - run 'gh aw compile' first")
+		return nil, errors.New("workflow has not been compiled yet - run 'gh aw compile' first")
 	}
 
 	// Read the lock file - path is sanitized using filepath.Clean() to prevent path traversal attacks.
 	// The lockPath is derived from markdownPath which comes from trusted sources (CLI arguments, validated workflow paths).
 	contentBytes, err := os.ReadFile(cleanLockPath) // #nosec G304
 	if err != nil {
-		return false, fmt.Errorf("failed to read lock file: %w", err)
+		return nil, fmt.Errorf("failed to read lock file: %w", err)
 	}
 
-	// Parse the YAML content
 	var workflowYAML map[string]any
 	if err := yaml.Unmarshal(contentBytes, &workflowYAML); err != nil {
-		return false, fmt.Errorf("failed to parse lock file YAML: %w", err)
+		return nil, fmt.Errorf("failed to parse lock file YAML: %w", err)
+	}
+
+	return workflowYAML, nil
+}
+
+// IsRunnable checks if a workflow can be run (has schedule or workflow_dispatch trigger)
+// This function checks the compiled .lock.yml file because that's what GitHub Actions uses.
+func IsRunnable(markdownPath string) (bool, error) {
+	validationLog.Printf("Checking if workflow is runnable: markdown=%s, lock=%s", markdownPath, getLockFilePath(markdownPath))
+
+	workflowYAML, err := readLockFile(markdownPath)
+	if err != nil {
+		return false, err
 	}
 
-	// Check if 'on' section is present
 	onSection, exists := workflowYAML["on"]
 	if !exists {
-		validationLog.Printf("No 'on' section found in lock file")
-		// If no 'on' section, it's not runnable
 		return false, nil
 	}
 
-	// Convert to map if possible
 	onMap, ok := onSection.(map[string]any)
 	if !ok {
 		// If 'on' is not a map, check if it's a string/list that might indicate workflow_dispatch
-		onStr := fmt.Sprintf("%v", onSection)
-		onStrLower := strings.ToLower(onStr)
-		hasWorkflowDispatch := strings.Contains(onStrLower, "workflow_dispatch")
+		hasWorkflowDispatch := strings.Contains(strings.ToLower(fmt.Sprintf("%v", onSection)), "workflow_dispatch")
 		validationLog.Printf("On section is not a map, checking string: hasWorkflowDispatch=%v", hasWorkflowDispatch)
 		return hasWorkflowDispatch, nil
 	}
 
-	// Check if workflow_dispatch trigger exists
 	_, hasWorkflowDispatch := onMap["workflow_dispatch"]
 	validationLog.Printf("Workflow runnable check: hasWorkflowDispatch=%v", hasWorkflowDispatch)
 	return hasWorkflowDispatch, nil
@@ -84,69 +84,43 @@ func IsRunnable(markdownPath string) (bool, error) {
 // getWorkflowInputs extracts workflow_dispatch inputs from the compiled lock file
 // This function checks the .lock.yml file because that's what GitHub Actions uses.
 func getWorkflowInputs(markdownPath string) (map[string]*workflow.InputDefinition, error) {
-	// Convert markdown path to lock file path
-	lockPath := getLockFilePath(markdownPath)
-	cleanLockPath := filepath.Clean(lockPath)
-
-	validationLog.Printf("Extracting workflow inputs from lock file: %s", lockPath)
-
-	// Check if the lock file exists
-	if _, err := os.Stat(cleanLockPath); os.IsNotExist(err) {
-		validationLog.Printf("Lock file does not exist: %s", cleanLockPath)
-		return nil, errors.New("workflow has not been compiled yet - run 'gh aw compile' first")
-	}
+	validationLog.Printf("Extracting workflow inputs from lock file: %s", getLockFilePath(markdownPath))
 
-	// Read the lock file - path is sanitized using filepath.Clean() to prevent path traversal attacks.
-	// The lockPath is derived from markdownPath which comes from trusted sources (CLI arguments, validated workflow paths).
-	contentBytes, err := os.ReadFile(cleanLockPath) // #nosec G304
+	workflowYAML, err := readLockFile(markdownPath)
 	if err != nil {
-		return nil, fmt.Errorf("failed to read lock file: %w", err)
-	}
-
-	// Parse the YAML content
-	var workflowYAML map[string]any
-	if err := yaml.Unmarshal(contentBytes, &workflowYAML); err != nil {
-		return nil, fmt.Errorf("failed to parse lock file YAML: %w", err)
+		return nil, err
 	}
 
-	// Check if 'on' section is present
 	onSection, exists := workflowYAML["on"]
 	if !exists {
 		return nil, nil
 	}
 
-	// Convert to map if possible
 	onMap, ok := onSection.(map[string]any)
 	if !ok {
 		return nil, nil
 	}
 
-	// Get workflow_dispatch section
 	workflowDispatch, exists := onMap["workflow_dispatch"]
 	if !exists {
 		return nil, nil
 	}
 
-	// Convert to map
 	workflowDispatchMap, ok := workflowDispatch.(map[string]any)
 	if !ok {
-		// workflow_dispatch might be null/empty
 		return nil, nil
 	}
 
-	// Get inputs section
 	inputsSection, exists := workflowDispatchMap["inputs"]
 	if !exists {
 		return nil, nil
 	}
 
-	// Convert to map
 	inputsMap, ok := inputsSection.(map[string]any)
 	if !ok {
 		return nil, nil
 	}
 
-	// Parse input definitions
 	return workflow.ParseInputDefinitions(inputsMap), nil
 }
 
diff --git a/pkg/workflow/project_config_parsing.go b/pkg/workflow/project_config_parsing.go
index 59e70164b55..d47d773a77f 100644
--- a/pkg/workflow/project_config_parsing.go
+++ b/pkg/workflow/project_config_parsing.go
@@ -20,27 +20,11 @@ func parseProjectViews(configMap map[string]any, log *logger.Logger) []ProjectVi
 		if !ok {
 			continue
 		}
-		view := ProjectView{}
-
-		// Parse name (required)
-		if name, exists := viewMap["name"]; exists {
-			if nameStr, ok := name.(string); ok {
-				view.Name = nameStr
-			}
-		}
-
-		// Parse layout (required)
-		if layout, exists := viewMap["layout"]; exists {
-			if layoutStr, ok := layout.(string); ok {
-				view.Layout = layoutStr
-			}
-		}
-
-		// Parse filter (optional)
-		if filter, exists := viewMap["filter"]; exists {
-			if filterStr, ok := filter.(string); ok {
-				view.Filter = filterStr
-			}
+		view := ProjectView{
+			Name:        extractStringFromMap(viewMap, "name", nil),
+			Layout:      extractStringFromMap(viewMap, "layout", nil),
+			Filter:      extractStringFromMap(viewMap, "filter", nil),
+			Description: extractStringFromMap(viewMap, "description", nil),
 		}
 
 		// Parse visible-fields (optional)
@@ -54,13 +38,6 @@ func parseProjectViews(configMap map[string]any, log *logger.Logger) []ProjectVi
 			}
 		}
 
-		// Parse description (optional)
-		if description, exists := viewMap["description"]; exists {
-			if descStr, ok := description.(string); ok {
-				view.Description = descStr
-			}
-		}
-
 		// Only add view if it has required fields
 		if view.Name != "" && view.Layout != "" {
 			views = append(views, view)
@@ -95,20 +72,12 @@ func parseProjectFieldDefinitions(configMap map[string]any, log *logger.Logger)
 			continue
 		}
 
-		field := ProjectFieldDefinition{}
-
-		if name, exists := fieldMap["name"]; exists {
-			if nameStr, ok := name.(string); ok {
-				field.Name = nameStr
-			}
-		}
-
-		dataType, hasDataType := fieldMap["data-type"]
-		if !hasDataType {
-			dataType = fieldMap["data_type"]
+		field := ProjectFieldDefinition{
+			Name:     extractStringFromMap(fieldMap, "name", nil),
+			DataType: extractStringFromMap(fieldMap, "data-type", nil),
 		}
-		if dataTypeStr, ok := dataType.(string); ok {
-			field.DataType = dataTypeStr
+		if field.DataType == "" {
+			field.DataType = extractStringFromMap(fieldMap, "data_type", nil)
 		}
 
 		if options, exists := fieldMap["options"]; exists {
