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
12 changes: 12 additions & 0 deletions .changeset/shellcheck-opt-in-default.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 8 additions & 6 deletions cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ type compileCmdOptions struct {
grype bool
grant bool
yamllint bool
noShellcheck bool
shellcheck bool
jsonOutput bool
showAllErrors bool
fix bool
Expand Down Expand Up @@ -465,7 +465,7 @@ func getCompileCmdOptions(cmd *cobra.Command) compileCmdOptions {
grype, _ := cmd.Flags().GetBool("grype")
grant, _ := cmd.Flags().GetBool("grant")
yamllint, _ := cmd.Flags().GetBool("yamllint")
noShellcheck, _ := cmd.Flags().GetBool("no-shellcheck")
shellcheck, _ := cmd.Flags().GetBool("shellcheck")
jsonOutput, _ := cmd.Flags().GetBool("json")
showAllErrors, _ := cmd.Flags().GetBool("show-all")
fix, _ := cmd.Flags().GetBool("fix")
Expand All @@ -485,7 +485,7 @@ func getCompileCmdOptions(cmd *cobra.Command) compileCmdOptions {
dir: dir, workflowsDir: workflowsDir, logicalRepo: logicalRepo, scheduleSeed: scheduleSeed, priorManifestFile: priorManifestFile,
validate: validate, watch: watch, noEmit: noEmit, purge: purge, strict: strict, trial: trial, dependabot: dependabot,
forceOverwrite: forceOverwrite, refreshStopTime: refreshStopTime, forceRefreshActionPins: forceRefreshActionPins, allowActionRefs: allowActionRefs,
zizmor: zizmor, poutine: poutine, actionlint: actionlint, runnerGuard: runnerGuard, syft: syft, grype: grype, grant: grant, yamllint: yamllint, noShellcheck: noShellcheck,
zizmor: zizmor, poutine: poutine, actionlint: actionlint, runnerGuard: runnerGuard, syft: syft, grype: grype, grant: grant, yamllint: yamllint, shellcheck: shellcheck,
jsonOutput: jsonOutput, showAllErrors: showAllErrors, fix: fix, stats: stats, failFast: failFast, noCheckUpdate: noCheckUpdate,
staged: staged, approve: approve, validateImages: validateImages, ghes: ghes, verbose: verbose, useSamples: useSamples,
}
Expand Down Expand Up @@ -518,7 +518,7 @@ func (o *compileCmdOptions) toCompileConfig(args []string) cli.CompileConfig {
NoEmit: o.noEmit, Purge: o.purge, TrialMode: o.trial, TrialLogicalRepoSlug: o.logicalRepo, Strict: o.strict,
Dependabot: o.dependabot, ForceOverwrite: o.forceOverwrite, RefreshStopTime: o.refreshStopTime, ForceRefreshActionPins: o.forceRefreshActionPins,
AllowActionRefs: o.allowActionRefs, Zizmor: o.zizmor, Poutine: o.poutine, Actionlint: o.actionlint, RunnerGuard: o.runnerGuard,
Syft: o.syft, Grype: o.grype, Grant: o.grant, Yamllint: o.yamllint, NoShellcheck: o.noShellcheck, JSONOutput: o.jsonOutput, ShowAllErrors: o.showAllErrors,
Syft: o.syft, Grype: o.grype, Grant: o.grant, Yamllint: o.yamllint, Shellcheck: o.shellcheck, JSONOutput: o.jsonOutput, ShowAllErrors: o.showAllErrors,
Stats: o.stats, FailFast: o.failFast, ScheduleSeed: o.scheduleSeed, Staged: o.staged, Approve: o.approve,
ValidateImages: o.validateImages, PriorManifestFile: o.priorManifestFile, GHESCompat: o.ghes, UseSamples: o.useSamples,
}
Expand Down Expand Up @@ -747,7 +747,7 @@ func configureCompileBuildFlags() {
compileCmd.Flags().String("action-tag", "", "Pin compiled workflows to a specific version of gh-aw actions. Accepts a full commit SHA or a version tag (e.g. v1, v1.2.3). Sets --action-mode to 'release' unless --action-mode action is also specified. Cannot be combined with --gh-aw-ref; use --gh-aw-ref when you want to resolve a branch or tag name to its current SHA")
compileCmd.Flags().String("actions-repo", "", "Override the external actions repository used in action mode (default: github/gh-aw-actions)")
compileCmd.Flags().String("gh-aw-ref", "", "Pin compiled workflows to a specific branch, tag, or commit SHA of github/gh-aw (e.g. main, my-feature, abc123). Branch and tag names are resolved to their full commit SHA at compile time so the baked-in ref is immutable. Equivalent to --action-mode release --action-tag <resolved-sha>. Cannot be combined with --action-tag or --action-mode. Use this to E2E-test workflows against a specific gh-aw revision")
compileCmd.Flags().Bool("validate", false, "Enable GitHub Actions workflow schema validation, container image validation, and action SHA validation")
compileCmd.Flags().Bool("validate", false, "Enable GitHub Actions workflow schema validation, container image validation, action SHA validation, and run step shell linting (shellcheck)")
compileCmd.Flags().BoolP("watch", "w", false, "Watch for changes to workflow files and recompile automatically")
compileCmd.Flags().StringP("dir", "d", "", "Workflow directory (default: $GH_AW_WORKFLOWS_DIR or .github/workflows)")
compileCmd.Flags().String("workflows-dir", "", "Deprecated: use --dir instead")
Expand Down Expand Up @@ -775,7 +775,9 @@ func configureCompileToolFlags() {
compileCmd.Flags().Bool("grype", false, "Run grype vulnerability scanner on container images referenced in compiled .lock.yml files (uses Docker image "+cli.GrypeImage+")")
compileCmd.Flags().Bool("grant", false, "Run grant license scanner on container images referenced in compiled .lock.yml files (uses Docker image "+cli.GrantImage+")")
compileCmd.Flags().Bool("yamllint", false, "Run yamllint YAML linter on generated .lock.yml files (uses Docker image "+cli.YamllintImage+")")
compileCmd.Flags().Bool("no-shellcheck", false, "Disable shellcheck linting of run step scripts (shellcheck runs by default when available)")
compileCmd.Flags().Bool("shellcheck", false, "Run shellcheck linting of run step scripts (also enabled by --validate)")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 09a6d65. Added a major changeset (.changeset/shellcheck-opt-in-default.md) documenting the breaking behavior change and migration guide. Also retained --no-shellcheck as a deprecated no-op so existing scripts continue to work — it prints a deprecation warning when used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 09a6d65. Updated --validate description to: "Enable GitHub Actions workflow schema validation, container image validation, action SHA validation, and run step shell linting (shellcheck)".

compileCmd.Flags().Bool("no-shellcheck", false, "Deprecated: shellcheck is now opt-in via --shellcheck; this flag is a no-op and will be removed in a future release")
_ = compileCmd.Flags().MarkDeprecated("no-shellcheck", "shellcheck is now opt-in; use --shellcheck to enable it. This flag has no effect and will be removed in a future release")
compileCmd.Flags().Bool("fix", false, "Apply automatic codemod fixes to workflows before compiling")
compileCmd.Flags().BoolP("json", "j", false, "Output results in JSON format")
compileCmd.Flags().Bool("show-all", false, "Display all compilation errors instead of only the highest-priority subset (default: top 5)")
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/compile_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,8 +509,8 @@ func TestCompileConfig_DefaultValues(t *testing.T) {
if config.Actionlint {
t.Error("Expected Actionlint to default to false")
}
if config.NoShellcheck {
t.Error("Expected NoShellcheck to default to false (shellcheck runs by default when available)")
if config.Shellcheck {
t.Error("Expected Shellcheck to default to false (shellcheck is disabled by default)")
}
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/compile_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type CompileConfig struct {
Grype bool // Run grype vulnerability scanner on container images referenced in compiled .lock.yml files
Grant bool // Run grant license scanner on container images referenced in compiled .lock.yml files
Yamllint bool // Run yamllint YAML linter on generated .lock.yml files
NoShellcheck bool // Skip shellcheck linting of run step scripts (shellcheck runs by default when available)
Shellcheck bool // Run shellcheck linting of run step scripts (disabled by default; enabled by --shellcheck or --validate)
JSONOutput bool // Output validation results as JSON
ShowAllErrors bool // Display all prioritized errors instead of the default top five
ActionMode string // How action scripts are referenced: dev, release, or action. Auto-detected if empty.
Expand Down
13 changes: 6 additions & 7 deletions pkg/cli/compile_orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,15 @@ func CompileWorkflows(ctx context.Context, config CompileConfig) ([]*workflow.Wo
initActionlintStats()
}

// Warn or error when shellcheck is enabled (the default) but not installed.
// Warn or error when shellcheck is requested (via --shellcheck or --validate) but not installed.
// Skip this check when --no-emit is set: no lock files are written so shellcheck
// is never invoked, regardless of the --strict or --validate flags.
// When the binary is absent, Docker is used as a fallback (lazy — only when
// there are scripts to lint). Only warn/error when neither is available.
if !config.NoShellcheck && !config.NoEmit && !isShellcheckAvailable() {
// is never invoked. When the binary is absent, Docker is used as a fallback (lazy
// — only when there are scripts to lint). Only warn/error when neither is available.
if (config.Shellcheck || config.Validate) && !config.NoEmit && !isShellcheckAvailable() {
if !IsDockerAvailable(ctx) {
if config.Strict {
return nil, errors.New("shellcheck not available: binary not found in PATH and Docker is not running; install shellcheck or start Docker to enable run step linting, or use --no-shellcheck to skip")
} else if config.Validate {
return nil, errors.New("shellcheck not available: binary not found in PATH and Docker is not running; install shellcheck or start Docker to enable run step linting")
} else {
fmt.Fprintln(os.Stderr, console.FormatWarningMessageStderr("shellcheck binary not found in PATH and Docker is not running; run step linting will be skipped"))
}
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/cli/compile_pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func compileSpecificFiles(
if config.Yamllint {
lockFilesForYamllint = append(lockFilesForYamllint, fileResult.lockFile)
}
if !config.NoShellcheck {
if config.Shellcheck || config.Validate {
lockFilesForShellcheck = append(lockFilesForShellcheck, fileResult.lockFile)
}
}
Expand Down Expand Up @@ -297,7 +297,7 @@ func compileSpecificFiles(
}

// Run shellcheck on run step scripts in all collected lock files.
if !config.NoShellcheck && !config.NoEmit && len(lockFilesForShellcheck) > 0 {
if (config.Shellcheck || config.Validate) && !config.NoEmit && len(lockFilesForShellcheck) > 0 {
if err := ctx.Err(); err != nil {
return workflowDataList, err
}
Expand Down Expand Up @@ -486,7 +486,7 @@ func compileAllFilesInDirectory(
if config.Yamllint {
lockFilesForYamllint = append(lockFilesForYamllint, fileResult.lockFile)
}
if !config.NoShellcheck {
if config.Shellcheck || config.Validate {
lockFilesForShellcheck = append(lockFilesForShellcheck, fileResult.lockFile)
}
}
Expand Down Expand Up @@ -605,7 +605,7 @@ func compileAllFilesInDirectory(
}

// Run shellcheck on run step scripts in all collected lock files.
if !config.NoShellcheck && !config.NoEmit && len(lockFilesForShellcheck) > 0 {
if (config.Shellcheck || config.Validate) && !config.NoEmit && len(lockFilesForShellcheck) > 0 {
if err := ctx.Err(); err != nil {
return workflowDataList, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/compile_pipeline_yamllint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ This is a test workflow for yamllint batch execution.
NoEmit: false,
Yamllint: true,
Strict: strict,
NoShellcheck: true, // yamllint test is independent of shellcheck
// Shellcheck is disabled by default; yamllint test is independent of shellcheck.
}

_, err = CompileWorkflows(context.Background(), config)
Expand Down
19 changes: 0 additions & 19 deletions pkg/cli/mcp_tools_readonly.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ Returns JSON array with validation results for each workflow:
// the caller knows linting was skipped, while preserving each workflow's
// valid/invalid status.
var dockerUnavailableWarning string
var shellcheckUnavailableWarning string

// Check if any static analysis tools are requested that require Docker images
if args.Zizmor || args.Poutine || args.Actionlint || args.RunnerGuard || args.Syft || args.Grype || args.Grant || args.Yamllint {
Expand Down Expand Up @@ -197,14 +196,6 @@ Returns JSON array with validation results for each workflow:
// Always validate workflows during compilation and use JSON output for MCP.
cmdArgs := []string{"compile", "--validate", "--json"}

// Keep compile JSON responses usable in MCP on hosts without shellcheck support.
// When neither a native shellcheck binary nor Docker fallback is available,
// skip shellcheck for this subprocess and return a structured warning.
if !isShellcheckAvailable() && !IsDockerAvailable(ctx) {
cmdArgs = append(cmdArgs, "--no-shellcheck")
shellcheckUnavailableWarning = "shellcheck binary not found in PATH and Docker is not running; run step linting was skipped"
}

// Add fix flag if requested
if args.Fix {
cmdArgs = append(cmdArgs, "--fix")
Expand Down Expand Up @@ -301,9 +292,6 @@ Returns JSON array with validation results for each workflow:
if dockerUnavailableWarning != "" {
outputStr = injectDockerUnavailableWarning(outputStr, dockerUnavailableWarning)
}
if shellcheckUnavailableWarning != "" {
outputStr = injectShellcheckUnavailableWarning(outputStr, shellcheckUnavailableWarning)
}
outputStr = injectShellcheckDiagnostics(outputStr, string(stderr))

return &mcp.CallToolResult{
Expand Down Expand Up @@ -522,13 +510,6 @@ func injectDockerUnavailableWarning(outputStr, warningMsg string) string {
})
}

func injectShellcheckUnavailableWarning(outputStr, warningMsg string) string {
return injectValidationWarning(outputStr, CompileValidationError{
Type: "shellcheck_unavailable",
Message: warningMsg,
})
}

func injectShellcheckDiagnostics(outputStr, stderrOutput string) string {
diagnostics := extractShellcheckDiagnostics(stderrOutput)
if len(diagnostics) == 0 {
Expand Down
Loading
Loading