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.
(markdownPath string)
| 32 | // IsRunnable checks if a workflow can be run (has schedule or workflow_dispatch trigger) |
| 33 | // This function checks the compiled .lock.yml file because that's what GitHub Actions uses. |
| 34 | func IsRunnable(markdownPath string) (bool, error) { |
| 35 | // Convert markdown path to lock file path |
| 36 | lockPath := getLockFilePath(markdownPath) |
| 37 | cleanLockPath := filepath.Clean(lockPath) |
| 38 | |
| 39 | validationLog.Printf("Checking if workflow is runnable: markdown=%s, lock=%s", markdownPath, lockPath) |
| 40 | |
| 41 | // Check if the lock file exists |
| 42 | if _, err := os.Stat(cleanLockPath); os.IsNotExist(err) { |
| 43 | validationLog.Printf("Lock file does not exist: %s", cleanLockPath) |
| 44 | return false, errors.New("workflow has not been compiled yet - run 'gh aw compile' first") |
| 45 | } |
| 46 | |
| 47 | // Read the lock file - path is sanitized using filepath.Clean() to prevent path traversal attacks. |
| 48 | // The lockPath is derived from markdownPath which comes from trusted sources (CLI arguments, validated workflow paths). |
| 49 | contentBytes, err := os.ReadFile(cleanLockPath) // #nosec G304 -- path is sanitized with filepath.Clean() and derived from trusted CLI argument |
| 50 | if err != nil { |
| 51 | return false, fmt.Errorf("failed to read lock file: %w", err) |
| 52 | } |
| 53 | |
| 54 | // Parse the YAML content |
| 55 | var workflowYAML map[string]any |
| 56 | if err := yaml.Unmarshal(contentBytes, &workflowYAML); err != nil { |
| 57 | return false, fmt.Errorf("failed to parse lock file YAML: %w", err) |
| 58 | } |
| 59 | |
| 60 | // Check if 'on' section is present |
| 61 | onSection, exists := workflowYAML["on"] |
| 62 | if !exists { |
| 63 | validationLog.Printf("No 'on' section found in lock file") |
| 64 | // If no 'on' section, it's not runnable |
| 65 | return false, nil |
| 66 | } |
| 67 | |
| 68 | // Convert to map if possible |
| 69 | onMap, ok := onSection.(map[string]any) |
| 70 | if !ok { |
| 71 | // If 'on' is not a map, check if it's a string/list that might indicate workflow_dispatch |
| 72 | onStr := fmt.Sprintf("%v", onSection) |
| 73 | onStrLower := strings.ToLower(onStr) |
| 74 | hasWorkflowDispatch := strings.Contains(onStrLower, "workflow_dispatch") |
| 75 | validationLog.Printf("On section is not a map, checking string: hasWorkflowDispatch=%v", hasWorkflowDispatch) |
| 76 | return hasWorkflowDispatch, nil |
| 77 | } |
| 78 | |
| 79 | // Check if workflow_dispatch trigger exists |
| 80 | _, hasWorkflowDispatch := onMap["workflow_dispatch"] |
| 81 | validationLog.Printf("Workflow runnable check: hasWorkflowDispatch=%v", hasWorkflowDispatch) |
| 82 | return hasWorkflowDispatch, nil |
| 83 | } |
| 84 | |
| 85 | // getWorkflowInputs extracts workflow_dispatch inputs from the compiled lock file |
| 86 | // This function checks the .lock.yml file because that's what GitHub Actions uses. |