diff --git a/pkg/cli/run_workflow_validation.go b/pkg/cli/run_workflow_validation.go index b768065d8b..d4dc7bf1f7 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 59e70164b5..d47d773a77 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 {