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
70 changes: 22 additions & 48 deletions pkg/cli/run_workflow_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines 66 to 69

// 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
Expand All @@ -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
}

Expand Down
51 changes: 10 additions & 41 deletions pkg/workflow/project_config_parsing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
Comment on lines +75 to 81

if options, exists := fieldMap["options"]; exists {
Expand Down
Loading