diff --git a/.github/prompts/create-agentic-workflow.prompt.md b/.github/prompts/create-agentic-workflow.prompt.md index 30018980913..769d4a1ed67 100644 --- a/.github/prompts/create-agentic-workflow.prompt.md +++ b/.github/prompts/create-agentic-workflow.prompt.md @@ -45,6 +45,11 @@ Analyze the user's response and map it to agentic workflows. Ask clarifying ques - āš ļø If you think the task requires **network access beyond localhost**, explicitly ask about configuring the top-level `network:` allowlist (ecosystems like `node`, `python`, `playwright`, or specific domains). - šŸ’” If you detect the task requires **browser automation**, suggest the **`playwright`** tool. +**Scheduling Best Practices:** + - šŸ“… When creating a **daily scheduled workflow**, analyze existing workflows to find an unused hour. Check `.github/workflows/*.md` for existing `schedule:` triggers and suggest an available hour. + - 🚫 **Avoid weekend scheduling**: For daily workflows, use `cron: "0 * * 1-5"` to run only on weekdays (Monday-Friday) instead of `* * *` which includes weekends. + - Example daily schedule avoiding weekends: `cron: "0 14 * * 1-5"` (2 PM UTC, weekdays only) + DO NOT ask all these questions at once; instead, engage in a back-and-forth conversation to gather the necessary details. 4. **Tools & MCP Servers** diff --git a/pkg/cli/audit.go b/pkg/cli/audit.go index 7bb9194ad25..df412e514ef 100644 --- a/pkg/cli/audit.go +++ b/pkg/cli/audit.go @@ -14,10 +14,13 @@ import ( "github.com/githubnext/gh-aw/pkg/console" "github.com/githubnext/gh-aw/pkg/constants" + "github.com/githubnext/gh-aw/pkg/logger" "github.com/githubnext/gh-aw/pkg/workflow" "github.com/spf13/cobra" ) +var auditLog = logger.New("cli:audit") + // NewAuditCommand creates the audit command func NewAuditCommand() *cobra.Command { auditCmd := &cobra.Command{ @@ -148,11 +151,14 @@ func isPermissionError(err error) bool { // AuditWorkflowRun audits a single workflow run and generates a report func AuditWorkflowRun(runInfo RunURLInfo, outputDir string, verbose bool, parse bool, jsonOutput bool) error { + auditLog.Printf("Starting audit for workflow run: runID=%d, owner=%s, repo=%s", runInfo.RunID, runInfo.Owner, runInfo.Repo) + if verbose { fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Auditing workflow run %d...", runInfo.RunID))) } runOutputDir := filepath.Join(outputDir, fmt.Sprintf("run-%d", runInfo.RunID)) + auditLog.Printf("Using output directory: %s", runOutputDir) // Check if we have locally cached artifacts first hasLocalCache := dirExists(runOutputDir) && !isDirEmpty(runOutputDir) @@ -188,10 +194,12 @@ func AuditWorkflowRun(runInfo RunURLInfo, outputDir string, verbose bool, parse } // Download artifacts for the run + auditLog.Printf("Downloading artifacts for run %d", runInfo.RunID) err := downloadRunArtifacts(runInfo.RunID, runOutputDir, verbose) if err != nil { // Gracefully handle cases where the run legitimately has no artifacts if errors.Is(err, ErrNoArtifacts) { + auditLog.Printf("No artifacts found for run %d", runInfo.RunID) if verbose { fmt.Fprintln(os.Stderr, console.FormatWarningMessage("No artifacts attached to this run. Proceeding with metadata-only audit.")) } diff --git a/pkg/cli/run_command.go b/pkg/cli/run_command.go index 6271cefe4ad..f4e8ea83dbf 100644 --- a/pkg/cli/run_command.go +++ b/pkg/cli/run_command.go @@ -12,11 +12,16 @@ import ( "github.com/githubnext/gh-aw/pkg/console" "github.com/githubnext/gh-aw/pkg/constants" + "github.com/githubnext/gh-aw/pkg/logger" "github.com/githubnext/gh-aw/pkg/parser" ) +var runLog = logger.New("cli:run_command") + // RunWorkflowOnGitHub runs an agentic workflow on GitHub Actions func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride string, repoOverride string, autoMergePRs bool, verbose bool) error { + runLog.Printf("Starting workflow run: workflow=%s, enable=%v, engineOverride=%s, repo=%s", workflowIdOrName, enable, engineOverride, repoOverride) + if workflowIdOrName == "" { return fmt.Errorf("workflow name or ID is required") } @@ -32,12 +37,14 @@ func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride st // Validate workflow exists and is runnable if repoOverride != "" { + runLog.Printf("Validating remote workflow: %s in repo %s", workflowIdOrName, repoOverride) // For remote repositories, use remote validation if err := validateRemoteWorkflow(workflowIdOrName, repoOverride, verbose); err != nil { return fmt.Errorf("failed to validate remote workflow: %w", err) } // Note: We skip local runnable check for remote workflows as we assume they are properly configured } else { + runLog.Printf("Validating local workflow: %s", workflowIdOrName) // For local workflows, use existing local validation workflowFile, err := resolveWorkflowFile(workflowIdOrName, verbose) if err != nil { @@ -53,6 +60,7 @@ func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride st if !runnable { return fmt.Errorf("workflow '%s' cannot be run on GitHub Actions - it must have 'workflow_dispatch' trigger", workflowIdOrName) } + runLog.Printf("Workflow is runnable: %s", workflowFile) } // Handle --enable flag logic: check workflow state and enable if needed @@ -198,12 +206,14 @@ func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride st } fmt.Printf("Successfully triggered workflow: %s\n", lockFileName) + runLog.Printf("Workflow triggered successfully: %s", lockFileName) // Try to get the latest run for this workflow to show a direct link // Add a delay to allow GitHub Actions time to register the new workflow run runInfo, runErr := getLatestWorkflowRunWithRetry(lockFileName, repoOverride, verbose) if runErr == nil && runInfo.URL != "" { fmt.Printf("\nšŸ”— View workflow run: %s\n", runInfo.URL) + runLog.Printf("Workflow run URL: %s (ID: %d)", runInfo.URL, runInfo.DatabaseID) } else if verbose && runErr != nil { fmt.Printf("Note: Could not get workflow run URL: %v\n", runErr) } diff --git a/pkg/parser/schema.go b/pkg/parser/schema.go index 7d60880b238..5b0ecdcd391 100644 --- a/pkg/parser/schema.go +++ b/pkg/parser/schema.go @@ -11,9 +11,12 @@ import ( "strings" "github.com/githubnext/gh-aw/pkg/console" + "github.com/githubnext/gh-aw/pkg/logger" "github.com/santhosh-tekuri/jsonschema/v6" ) +var schemaLog = logger.New("parser:schema") + //go:embed schemas/main_workflow_schema.json var mainWorkflowSchema string @@ -25,8 +28,11 @@ var mcpConfigSchema string // ValidateMainWorkflowFrontmatterWithSchema validates main workflow frontmatter using JSON schema func ValidateMainWorkflowFrontmatterWithSchema(frontmatter map[string]any) error { + schemaLog.Print("Validating main workflow frontmatter with schema") + // First run the standard schema validation if err := validateWithSchema(frontmatter, mainWorkflowSchema, "main workflow file"); err != nil { + schemaLog.Printf("Schema validation failed for main workflow: %v", err) return err } @@ -47,8 +53,11 @@ func ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter map[string // ValidateIncludedFileFrontmatterWithSchema validates included file frontmatter using JSON schema func ValidateIncludedFileFrontmatterWithSchema(frontmatter map[string]any) error { + schemaLog.Print("Validating included file frontmatter with schema") + // First run the standard schema validation if err := validateWithSchema(frontmatter, includedFileSchema, "included file"); err != nil { + schemaLog.Printf("Schema validation failed for included file: %v", err) return err } @@ -321,6 +330,7 @@ func validateEngineSpecificRules(frontmatter map[string]any) error { // Handle string format engine if engineStr, ok := engine.(string); ok { + schemaLog.Printf("Validating engine-specific rules for string engine: %s", engineStr) // String format doesn't support permissions, so no validation needed _ = engineStr return nil @@ -338,9 +348,12 @@ func validateEngineSpecificRules(frontmatter map[string]any) error { return nil // Missing or invalid ID, but this should be caught by schema validation } + schemaLog.Printf("Validating engine-specific rules for engine: %s", engineID) + // Check if codex engine has permissions configured if engineID == "codex" { if _, hasPermissions := engineMap["permissions"]; hasPermissions { + schemaLog.Printf("Codex engine has invalid permissions configuration") return errors.New("engine permissions are not supported for codex engine. Only Claude engine supports permissions configuration") } } diff --git a/pkg/workflow/expressions.go b/pkg/workflow/expressions.go index 94052ca251a..d54354a272d 100644 --- a/pkg/workflow/expressions.go +++ b/pkg/workflow/expressions.go @@ -6,8 +6,11 @@ import ( "unicode" "github.com/githubnext/gh-aw/pkg/constants" + "github.com/githubnext/gh-aw/pkg/logger" ) +var expressionsLog = logger.New("workflow:expressions") + // ConditionNode represents a node in a condition expression tree type ConditionNode interface { Render() string @@ -497,6 +500,8 @@ const ( // Supports && (AND), || (OR), ! (NOT), and parentheses for grouping // Example: "condition1 && (condition2 || !condition3)" func ParseExpression(expression string) (ConditionNode, error) { + expressionsLog.Printf("Parsing expression: %s", expression) + if strings.TrimSpace(expression) == "" { return nil, fmt.Errorf("empty expression") } @@ -504,6 +509,7 @@ func ParseExpression(expression string) (ConditionNode, error) { parser := &ExpressionParser{} tokens, err := parser.tokenize(expression) if err != nil { + expressionsLog.Printf("Failed to tokenize expression: %v", err) return nil, err } parser.tokens = tokens @@ -511,6 +517,7 @@ func ParseExpression(expression string) (ConditionNode, error) { result, err := parser.parseOrExpression() if err != nil { + expressionsLog.Printf("Failed to parse expression: %v", err) return nil, err } @@ -519,6 +526,7 @@ func ParseExpression(expression string) (ConditionNode, error) { return nil, fmt.Errorf("unexpected token '%s' at position %d", parser.current().value, parser.current().pos) } + expressionsLog.Printf("Successfully parsed expression with %d tokens", len(tokens)) return result, nil } @@ -761,6 +769,8 @@ func BreakLongExpression(expression string) []string { return []string{expression} } + expressionsLog.Printf("Breaking long expression: length=%d", len(expression)) + var lines []string current := "" i := 0 diff --git a/pkg/workflow/permissions.go b/pkg/workflow/permissions.go index dc17aad73d6..f5ac487628b 100644 --- a/pkg/workflow/permissions.go +++ b/pkg/workflow/permissions.go @@ -5,9 +5,12 @@ import ( "sort" "strings" + "github.com/githubnext/gh-aw/pkg/logger" "github.com/goccy/go-yaml" ) +var permissionsLog = logger.New("workflow:permissions") + // PermissionsParser provides functionality to parse and analyze GitHub Actions permissions type PermissionsParser struct { rawPermissions string @@ -20,6 +23,8 @@ type PermissionsParser struct { // NewPermissionsParser creates a new PermissionsParser instance func NewPermissionsParser(permissionsYAML string) *PermissionsParser { + permissionsLog.Print("Creating new permissions parser") + parser := &PermissionsParser{ rawPermissions: permissionsYAML, parsedPerms: make(map[string]string), @@ -31,9 +36,12 @@ func NewPermissionsParser(permissionsYAML string) *PermissionsParser { // parse parses the permissions YAML and populates the internal structures func (p *PermissionsParser) parse() { if p.rawPermissions == "" { + permissionsLog.Print("No permissions to parse") return } + permissionsLog.Printf("Parsing permissions YAML: length=%d", len(p.rawPermissions)) + // Remove the "permissions:" prefix if present and get just the YAML content yamlContent := strings.TrimSpace(p.rawPermissions) if strings.HasPrefix(yamlContent, "permissions:") { @@ -138,12 +146,16 @@ func (p *PermissionsParser) parse() { // HasContentsReadAccess returns true if the permissions allow reading contents func (p *PermissionsParser) HasContentsReadAccess() bool { + permissionsLog.Print("Checking contents read access") + // Handle shorthand permissions if p.isShorthand { switch p.shorthandValue { case "read-all", "write-all", "read", "write": + permissionsLog.Printf("Shorthand permissions grant contents read: %s", p.shorthandValue) return true case "none": + permissionsLog.Print("Shorthand 'none' denies contents read") return false } return false