Skip to content

feat: shellcheck disabled by default, opt-in via --shellcheck/--validate, parallel execution - #49880

Merged
pelikhan merged 5 commits into
mainfrom
copilot/improve-shellcheck-integration
Aug 3, 2026
Merged

feat: shellcheck disabled by default, opt-in via --shellcheck/--validate, parallel execution#49880
pelikhan merged 5 commits into
mainfrom
copilot/improve-shellcheck-integration

Conversation

Copilot AI commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Shellcheck previously ran by default (opt-out via --no-shellcheck), inconsistent with all other checkers (poutine, zizmor, actionlint, etc.) which are opt-in. This aligns shellcheck with that pattern and adds parallel execution.

Changes

  • CompileConfig: NoShellcheck boolShellcheck bool; shellcheck now off by default
  • --no-shellcheck removed, replaced with --shellcheck; --validate also enables shellcheck
  • Parallel execution: runShellcheckOnLockFiles now collects all steps across lock files first, then fans out via goroutines + sync.WaitGroup instead of sequential per-step loop
  • compile_orchestrator.go: availability warning/error only emitted when --shellcheck is explicit (not via --validate transitive enable), keeping MCP compile paths clean
  • mcp_tools_readonly.go: removed --no-shellcheck suppression, shellcheckUnavailableWarning, and injectShellcheckUnavailableWarning — no longer needed since shellcheck is opt-in and silently skips when unavailable

…idate, run in parallel

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title feat: improve shellcheck integration - disabled by default, parallel execution feat: shellcheck disabled by default, opt-in via --shellcheck/--validate, parallel execution Aug 3, 2026
Copilot AI requested a review from pelikhan August 3, 2026 01:40
@pelikhan
pelikhan marked this pull request as ready for review August 3, 2026 01:48
Copilot AI review requested due to automatic review settings August 3, 2026 01:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Makes shellcheck opt-in while retaining --validate integration and parallelizing run-step linting.

Changes:

  • Replaces --no-shellcheck with --shellcheck.
  • Enables shellcheck through --shellcheck or --validate.
  • Runs shellcheck steps concurrently and simplifies MCP handling.
Show a summary per file
File Description
cmd/gh-aw/main.go Adds and wires the new CLI flag.
pkg/cli/compile_command_test.go Updates default configuration assertions.
pkg/cli/compile_config.go Replaces the opt-out configuration field.
pkg/cli/compile_orchestrator.go Limits availability warnings to explicit requests.
pkg/cli/compile_pipeline.go Enables shellcheck conditionally in both pipelines.
pkg/cli/compile_pipeline_yamllint_test.go Removes obsolete shellcheck suppression.
pkg/cli/mcp_tools_readonly.go Removes unavailable-shellcheck warning injection.
pkg/cli/shellcheck.go Adds parallel shellcheck execution and aggregation.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 8/8 changed files
  • Comments generated: 5
  • Review effort level: Balanced

Comment thread pkg/cli/shellcheck.go
Comment on lines +428 to +430
for i, step := range allSteps {
wg.Add(1)
go func(idx int, s runStepInfo) {

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 shellcheckMaxConcurrency = 8 semaphore following the same pattern as forecast_compute.go:209-245. Each goroutine acquires a semaphore slot before launching the shellcheck process, preventing exhaustion of process/fd limits on large workflow sets.

Comment thread pkg/cli/shellcheck.go Outdated
Comment on lines +434 to +436
stepErr = runShellcheckOnScriptViaDocker(ctx, s, shellcheckDefaultIgnoreCodes, verbose)
} else {
stepErr = runShellcheckOnScript(step, shellcheckDefaultIgnoreCodes, verbose)
stepErr = runShellcheckOnScript(s, shellcheckDefaultIgnoreCodes, verbose)

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. Both helpers now return ([]byte, error) rather than writing to os.Stderr directly. The goroutine captures the returned bytes; after wg.Wait(), output is flushed to os.Stderr in step order, keeping each diagnostic block intact.

Comment thread pkg/cli/shellcheck.go
results[idx] = result{err: stepErr}
}(i, step)
}
wg.Wait()

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.

Added TestRunShellcheckOnLockFiles_MultiStep in 09a6d65. The test uses a Docker stub with three run steps (step-a, step-b, step-c) where only step-b triggers a finding. It verifies: (1) all 3 steps are invoked in both modes, (2) diagnostics are attributed to step-b, (3) non-strict mode returns nil despite the failing step, and (4) strict mode returns an error after all steps complete.

Comment thread cmd/gh-aw/main.go
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.

Comment thread cmd/gh-aw/main.go
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. Updated --validate description to: "Enable GitHub Actions workflow schema validation, container image validation, action SHA validation, and run step shell linting (shellcheck)".

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR does not have the implementation label and has 64 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /codebase-design — requesting changes on three correctness/UX issues in the parallel shellcheck implementation.

📋 Key Themes & Highlights

Issues Found

  1. Availability warning gap (compile_orchestrator.go:71) — the guard that warns when shellcheck is unavailable only fires for --shellcheck, not for --validate. Users who pass --validate get silent skip.
  2. Strict-mode no longer fails fast (shellcheck.go:457) — the old sequential loop aborted on the first failure; the new parallel implementation waits for all goroutines before checking errors. This is a silent semantic change that may surprise users in CI who expect an early exit.
  3. Interleaved stderr in parallel runs (shellcheck.go:436) — both runShellcheckOnScript and runShellcheckOnScriptViaDocker write headers and bodies to os.Stderr directly inside goroutines, producing garbled output at scale.

Positive Highlights

  • ✅ Clean opt-in inversion; aligns shellcheck with poutine/zizmor/actionlint pattern
  • ✅ Dead code in mcp_tools_readonly.go cleanly removed
  • ✅ Good goroutine fan-out pattern with sync.WaitGroup and indexed results slice

Existing review comments (Copilot) already flag the unbounded goroutine pool and the missing multi-step test — those are worth addressing too.

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 38.3 AIC · ⌖ 11.1 AIC · ⊞ 7.1K
Comment /matt to run again

Comment thread pkg/cli/compile_orchestrator.go Outdated
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.NoEmit && !isShellcheckAvailable() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/codebase-design] Availability warning gap: this guard fires only for config.Shellcheck but the pipeline enables shellcheck for config.Validate too (see compile_pipeline.go). When --validate is passed without --shellcheck and neither the binary nor Docker is available, shellcheck silently skips with no user-facing feedback.

💡 Suggested fix

Change the condition to mirror the pipeline:

if (config.Shellcheck || config.Validate) && !config.NoEmit && !isShellcheckAvailable() {

That keeps the existing comment about explicit --shellcheck, or refine it to mention --validate as well.

@copilot please address this.

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. Changed the guard condition from config.Shellcheck to config.Shellcheck || config.Validate in compile_orchestrator.go so the availability warning/error fires when shellcheck is triggered via either flag.

Comment thread pkg/cli/shellcheck.go
shellcheckLog.Printf("shellcheck complete: steps=%d, issues=%d", len(allSteps), totalIssues)

if firstErr != nil {
if strict && firstErr != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/codebase-design] Strict mode now collects all errors before checking — it no longer fails fast. The old doc comment promised "fail fast in strict mode", and that semantic is gone. If a large codebase has hundreds of failing steps, all goroutines will still run to completion. Either preserve early cancellation via context.WithCancel or explicitly document that strict mode now reports-all-then-errors instead of failing on first.

💡 Option: cancel remaining goroutines on first error
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// inside goroutine, after detecting stepErr:
if stepErr != nil && strict {
    cancel()
}

Or, if the full-scan behaviour is intentional, update the function doc comment accordingly.

@copilot please address this.

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. Chose the "document it" path: the doc comment for runShellcheckOnLockFiles now explicitly states "In both strict and non-strict modes every step is checked before returning. When strict is true, any step failure causes a non-nil error to be returned after all steps have been checked (reports-all-then-errors)." The full-scan behaviour is intentional — it gives users a complete picture of all issues in one pass.

Comment thread pkg/cli/shellcheck.go Outdated
stepErr = runShellcheckOnScriptViaDocker(ctx, s, shellcheckDefaultIgnoreCodes, verbose)
} else {
stepErr = runShellcheckOnScript(step, shellcheckDefaultIgnoreCodes, verbose)
stepErr = runShellcheckOnScript(s, shellcheckDefaultIgnoreCodes, verbose)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/codebase-design] runShellcheckOnScript and runShellcheckOnScriptViaDocker write directly to os.Stderr inside goroutines. With many steps in flight simultaneously, their output lines interleave unpredictably. Collect each step's output into a buffer and flush sequentially after wg.Wait().

💡 Pattern
type result struct {
    err    error
    output []byte // captured stderr for this step
}
// flush in order after wg.Wait()
for _, r := range results {
    if len(r.output) > 0 {
        os.Stderr.Write(r.output)
    }
}

@copilot please address this.

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. runShellcheckOnScript and runShellcheckOnScriptViaDocker now return ([]byte, error) instead of writing directly to os.Stderr. Each goroutine captures its diagnostic output; after wg.Wait() completes, output is flushed to os.Stderr in step order so diagnostic blocks are never interleaved.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 31.3 AIC · ⌖ 9.42 AIC · ⊞ 5.4K

Comment thread pkg/cli/compile_orchestrator.go Outdated
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.NoEmit && !isShellcheckAvailable() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The availability guard only fires when config.Shellcheck is true, but compile_pipeline.go also enables shellcheck when config.Validate is true (via config.Shellcheck || config.Validate). When a user runs --validate on a host where neither shellcheck binary nor Docker is available, shellcheck is silently skipped — no warning, no error in strict mode.

The condition should mirror the pipeline:

if (config.Shellcheck || config.Validate) && !config.NoEmit && !isShellcheckAvailable() {

@copilot please address this.

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 — same as the companion thread above. The condition now mirrors the pipeline: (config.Shellcheck || config.Validate) && !config.NoEmit && !isShellcheckAvailable().

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

🔶 Test Quality Score: 30/100 — Needs improvement

Analyzed 2 test(s): 0 design, 2 implementation, 0 violation(s).

📊 Metrics (2 tests)
Metric Value
Analyzed 2 (Go: 2, JS: 0)
✅ Design 0 (0%)
⚠️ Implementation 2 (100%)
Edge/error coverage 0 (0%)
Duplicate clusters 0
Inflation No
🚨 Violations 0
Test File Classification Issues
TestCompileConfig (struct field addition) compile_command_test.go implementation_test Happy-path only; verifies struct field value, not behavior
TestCompileConfig_DefaultValues (Shellcheck check) compile_command_test.go implementation_test Verifies struct zero-value default, not runtime behavior
Comment-only change compile_pipeline_yamllint_test.go N/A
⚠️ Flagged Tests (2)

TestCompileConfig (compile_command_test.go) — implementation_test. Adds Shellcheck: false to a struct literal. Verifies a compile-time field value with no behavioral assertion. Low value if deleted.

TestCompileConfig_DefaultValues (compile_command_test.go) — implementation_test. Checks that config.Shellcheck is false on a zero-value struct. Confirms Go zero-value semantics, not that the flag correctly disables shellcheck at runtime.

Missing coverage for the core feature: The PR title is "shellcheck disabled by default, opt-in via --shellcheck/--validate, parallel execution". No test verifies: that shellcheck runs when Shellcheck: true is set; that shellcheck is skipped when Shellcheck: false; that --validate flag enables shellcheck; or parallel execution behavior.

Verdict

Failed. 100% implementation tests (threshold: 30%). The core behavioral contracts of the feature (opt-in flag triggers shellcheck, parallel execution) are untested.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 · 73.1 AIC · ⌖ 8.35 AIC · ⊞ 8.5K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ Test Quality Sentinel: 30/100. 100% implementation tests (threshold: 30%). The core behavioral contracts of the feature (opt-in flag triggers shellcheck, parallel execution) are untested. Review flagged tests in the comment above.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: Request changes\n\nBlocking issues remain unresolved from the prior review round — no new inline comments added to avoid duplication.\n\n
💡 Blocking themes (already flagged, unresolved)\n\n- Unbounded goroutine fan-out (shellcheck.go ~L420-441): every run step across all lock files now spawns its own goroutine invoking a subprocess or docker run with no concurrency cap. Large workflow sets can exhaust FDs / overwhelm the Docker daemon. Needs a bounded worker pool (semaphore or errgroup.SetLimit).\n- Concurrent stderr writes can interleave (shellcheck.go ~L436): runShellcheckOnScript/runShellcheckOnScriptViaDocker each write header + body to os.Stderr in separate calls; under the new parallel fan-out, output from different steps can interleave, corrupting diagnostics.\n- No test coverage for the new parallel fan-out/aggregation (shellcheck.go ~L441): existing tests only exercise a single-step lock file; nothing verifies multi-step invocation, error aggregation, or strict-mode fail behavior under concurrency.\n- Breaking CLI change without migration path (main.go ~L778): --no-shellcheck is removed and default behavior inverted (opt-in vs opt-out) with no changeset/migration note, despite repo policy requiring both for breaking CLI changes.\n- --validate help text is stale (main.go ~L778): --validate now also enables shellcheck as a side effect, but its flag description does not mention this, so users cannot anticipate the new linting from --help.\n\nThese should be addressed before merge; none are style nits — they affect resource exhaustion risk, correctness of concurrent output, test coverage of new concurrency logic, and CLI compatibility guarantees.\n\n

🔎 Code quality review by PR Code Quality Reviewer · auto · 79.9 AIC · ⌖ 5.66 AIC · ⊞ 7.9K
Comment /review to run again

@pelikhan

pelikhan commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@pelikhan

pelikhan commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

…-shellcheck no-op, multi-step tests, changeset

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Done. Addressed all 9 unresolved review threads in commit 09a6d65:

  • Bounded concurrency: semaphore (shellcheckMaxConcurrency = 8), matching forecast_compute.go pattern
  • Output buffering: runShellcheckOnScript/runShellcheckOnScriptViaDocker now return ([]byte, error); output flushed sequentially after wg.Wait()
  • Strict mode doc: updated to say "reports-all-then-errors" (intentional full-scan behaviour)
  • Availability warning: compile_orchestrator.go now checks config.Shellcheck || config.Validate
  • --validate help text: now mentions shellcheck
  • --no-shellcheck deprecated no-op: retained for script compatibility
  • Major changeset: .changeset/shellcheck-opt-in-default.md with migration guide
  • Multi-step test: TestRunShellcheckOnLockFiles_MultiStep — all 3 steps invoked, step-b diagnostics attributable, strict/non-strict aggregation verified

Local validation: make fmt, make lint, make test-unit all pass.

@pelikhan

pelikhan commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Ran pr-finisher pass and pushed a follow-up fix in dc8c41e to address the failing CGO lint finding (pkg/cli/shellcheck.go goroutine panic guard). Local validation is green (make fmt, make lint, make test-unit, make test, make recompile, make agent-report-progress). CI on this new HEAD still needs a maintainer-triggered rerun.

Copilot AI commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot fix https://github.com/github/gh-aw/actions/runs/30779905195/job/91583273034#step:8:1

Fixed in dc8c41e: added top-level panic recovery in the shellcheck worker goroutine at pkg/cli/shellcheck.go (the goroutinemissingrecover failure from step 8).

@pelikhan
pelikhan merged commit 846bb17 into main Aug 3, 2026
29 of 30 checks passed
@pelikhan
pelikhan deleted the copilot/improve-shellcheck-integration branch August 3, 2026 03:26
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.84.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants