Skip to content
Merged
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
3 changes: 3 additions & 0 deletions cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,7 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
hashCmd := cli.NewHashCommand()
projectCmd := cli.NewProjectCommand()
checksCmd := cli.NewChecksCommand()
validateCmd := cli.NewValidateCommand(validateEngine)

// Assign commands to groups
// Setup Commands
Expand All @@ -628,6 +629,7 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all

// Development Commands
compileCmd.GroupID = "development"
validateCmd.GroupID = "development"
mcpCmd.GroupID = "development"
statusCmd.GroupID = "development"
listCmd.GroupID = "development"
Expand Down Expand Up @@ -678,6 +680,7 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(secretsCmd)
rootCmd.AddCommand(fixCmd)
rootCmd.AddCommand(validateCmd)
rootCmd.AddCommand(completionCmd)
rootCmd.AddCommand(hashCmd)
rootCmd.AddCommand(projectCmd)
Expand Down
4 changes: 4 additions & 0 deletions docs/src/content/docs/reference/compilation-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,10 @@ Pre-activation runs checks sequentially. Any failure sets `activated=false`, pre
| `gh aw compile --actionlint --zizmor --poutine` | Run security scanners |
| `gh aw compile --purge` | Remove orphaned `.lock.yml` files |
| `gh aw compile --output /path/to/output` | Custom output directory |
| `gh aw validate` | Validate all workflows (compile + all linters, no file output) |
| `gh aw validate my-workflow` | Validate a specific workflow |
| `gh aw validate --json` | Validate and output results in JSON format |
| `gh aw validate --strict` | Validate with strict mode enforced |
Comment on lines +326 to +329
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs say gh aw validate runs “compile + all linters” with no file output, but the current implementation forces NoEmit: true, and the compile pipeline only runs zizmor/actionlint/poutine when !NoEmit. Either adjust the implementation so linters run in validate mode, or update this documentation to match actual behavior.

Suggested change
| `gh aw validate` | Validate all workflows (compile + all linters, no file output) |
| `gh aw validate my-workflow` | Validate a specific workflow |
| `gh aw validate --json` | Validate and output results in JSON format |
| `gh aw validate --strict` | Validate with strict mode enforced |
| `gh aw validate` | Validate all workflows (configuration only; no file output or security linters) |
| `gh aw validate my-workflow` | Validate configuration for a specific workflow (no file output or security linters) |
| `gh aw validate --json` | Validate configuration and output results in JSON format (no security linters) |
| `gh aw validate --strict` | Validate with strict mode enforced (configuration only; run linters via \`gh aw compile\` for security scanning) |

Copilot uses AI. Check for mistakes.

> [!TIP]
> Compilation is only required when changing **frontmatter configuration**. The **markdown body** (AI instructions) is loaded at runtime and can be edited without recompilation. See [Editing Workflows](/gh-aw/guides/editing-workflows/) for details.
Expand Down
91 changes: 91 additions & 0 deletions pkg/cli/validate_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package cli

import (
"context"

"github.com/github/gh-aw/pkg/constants"
"github.com/github/gh-aw/pkg/logger"
"github.com/spf13/cobra"
)

var validateLog = logger.New("cli:validate_command")

// NewValidateCommand creates the validate command
func NewValidateCommand(validateEngine func(string) error) *cobra.Command {
cmd := &cobra.Command{
Use: "validate [workflow]...",
Short: "Validate agentic workflows without generating lock files",
Long: `Validate one or more agentic workflows by compiling and running all linters without
generating lock files. This is equivalent to:

gh aw compile --validate --no-emit --zizmor --actionlint --poutine

If no workflows are specified, all Markdown files in .github/workflows will be validated.

` + WorkflowIDExplanation + `

Examples:
` + string(constants.CLIExtensionPrefix) + ` validate # Validate all workflows
` + string(constants.CLIExtensionPrefix) + ` validate ci-doctor # Validate a specific workflow
` + string(constants.CLIExtensionPrefix) + ` validate ci-doctor daily # Validate multiple workflows
` + string(constants.CLIExtensionPrefix) + ` validate workflow.md # Validate by file path
` + string(constants.CLIExtensionPrefix) + ` validate --dir custom/workflows # Validate from custom directory
` + string(constants.CLIExtensionPrefix) + ` validate --json # Output results in JSON format
` + string(constants.CLIExtensionPrefix) + ` validate --strict # Enforce strict mode validation
` + string(constants.CLIExtensionPrefix) + ` validate --fail-fast # Stop at the first error`,
RunE: func(cmd *cobra.Command, args []string) error {
engineOverride, _ := cmd.Flags().GetString("engine")
dir, _ := cmd.Flags().GetString("dir")
strict, _ := cmd.Flags().GetBool("strict")
jsonOutput, _ := cmd.Flags().GetBool("json")
failFast, _ := cmd.Flags().GetBool("fail-fast")
stats, _ := cmd.Flags().GetBool("stats")
noCheckUpdate, _ := cmd.Flags().GetBool("no-check-update")
verbose, _ := cmd.Flags().GetBool("verbose")

if err := validateEngine(engineOverride); err != nil {
return err
}

// Check for updates (non-blocking, runs once per day)
CheckForUpdatesAsync(cmd.Context(), noCheckUpdate, verbose)

validateLog.Printf("Running validate command: workflows=%v, dir=%s", args, dir)

config := CompileConfig{
MarkdownFiles: args,
Verbose: verbose,
EngineOverride: engineOverride,
Validate: true,
NoEmit: true,
Zizmor: true,
Actionlint: true,
Poutine: true,
WorkflowDir: dir,
Comment on lines +55 to +64
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NoEmit: true will prevent actionlint/zizmor/poutine from running at all. The compile pipeline only runs these tools when !config.NoEmit (e.g., pkg/cli/compile_orchestration.go:132-158 and similar), because it collects generated .lock.yml paths. As written, gh aw validate will effectively skip the external linters despite setting Zizmor/Actionlint/Poutine to true. Consider either (a) emitting lock files to a temp location and cleaning them up after linting, (b) allowing linters to run against in-memory output/temporary files even in no-emit mode, or (c) not forcing NoEmit when linters are requested.

Copilot uses AI. Check for mistakes.
Strict: strict,
JSONOutput: jsonOutput,
FailFast: failFast,
Stats: stats,
}
if _, err := CompileWorkflows(context.Background(), config); err != nil {
return err
Comment on lines +70 to +71
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CompileWorkflows is called with context.Background(), so cancellation/timeouts from Cobra (Ctrl-C, parent context) won’t propagate. This is inconsistent with the compile command which passes cmd.Context() (see cmd/gh-aw/main.go:322). Use cmd.Context() here instead.

Copilot uses AI. Check for mistakes.
}
return nil
},
}

cmd.Flags().StringP("engine", "e", "", "Override AI engine (claude, codex, copilot, custom)")
cmd.Flags().StringP("dir", "d", "", "Workflow directory (default: .github/workflows)")
cmd.Flags().Bool("strict", false, "Enforce strict mode validation for all workflows")
cmd.Flags().BoolP("json", "j", false, "Output results in JSON format")
cmd.Flags().Bool("fail-fast", false, "Stop at the first validation error instead of collecting all errors")
cmd.Flags().Bool("stats", false, "Display statistics table sorted by file size")
cmd.Flags().Bool("no-check-update", false, "Skip checking for gh-aw updates")

// Register completions
cmd.ValidArgsFunction = CompleteWorkflowNames
RegisterEngineFlagCompletion(cmd)
RegisterDirFlagCompletion(cmd, "dir")

return cmd
}
31 changes: 31 additions & 0 deletions pkg/cli/validate_command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//go:build !integration

package cli

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestNewValidateCommand tests that the validate command is created correctly
func TestNewValidateCommand(t *testing.T) {
cmd := NewValidateCommand(func(string) error { return nil })

require.NotNil(t, cmd, "NewValidateCommand should return a non-nil command")
assert.Equal(t, "validate", cmd.Name(), "Command name should be 'validate'")
assert.NotEmpty(t, cmd.Short, "Command should have a short description")
assert.NotEmpty(t, cmd.Long, "Command should have a long description")

// Verify key flags exist
require.NotNil(t, cmd.Flags().Lookup("dir"), "validate command should have a --dir flag")
assert.Equal(t, "d", cmd.Flags().Lookup("dir").Shorthand, "--dir flag should have -d shorthand")
require.NotNil(t, cmd.Flags().Lookup("json"), "validate command should have a --json flag")
assert.Equal(t, "j", cmd.Flags().Lookup("json").Shorthand, "--json flag should have -j shorthand")
require.NotNil(t, cmd.Flags().Lookup("engine"), "validate command should have a --engine flag")
require.NotNil(t, cmd.Flags().Lookup("strict"), "validate command should have a --strict flag")
require.NotNil(t, cmd.Flags().Lookup("fail-fast"), "validate command should have a --fail-fast flag")
require.NotNil(t, cmd.Flags().Lookup("stats"), "validate command should have a --stats flag")
require.NotNil(t, cmd.Flags().Lookup("no-check-update"), "validate command should have a --no-check-update flag")
}
2 changes: 2 additions & 0 deletions pkg/cli/verify_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package cli - verify_command.go is superseded by validate_command.go.
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new // Package cli ... comment is a package doc comment and may override/compete with the real package documentation in pkg/cli/doc.go when generating docs. Since the file is otherwise empty, consider deleting it, or at least move the comment below package cli (or change it to a non-package comment) to avoid impacting package docs.

Suggested change
// Package cli - verify_command.go is superseded by validate_command.go.
// verify_command.go is superseded by validate_command.go.

Copilot uses AI. Check for mistakes.
package cli
4 changes: 4 additions & 0 deletions pkg/cli/verify_command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//go:build !integration

// Package cli - verify_command_test.go is superseded by validate_command_test.go.
package cli
Loading