Skip to content

feat: fail compile --actionlint on high severity errors - #50874

Merged
pelikhan merged 1 commit into
mainfrom
copilot/ensure-actionlint-fails-on-errors
Aug 6, 2026
Merged

feat: fail compile --actionlint on high severity errors#50874
pelikhan merged 1 commit into
mainfrom
copilot/ensure-actionlint-fails-on-errors

Conversation

Copilot AI commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

compile --actionlint in non-strict mode was ignoring all findings, including genuine high-severity errors. Only strict mode would fail, making the --actionlint flag ineffective as a gate for real problems.

Ref: https://github.com/github/gh-aw/actions/runs/31103313197/job/92623368612#step:5:1

Changes

  • Severity classification: Non-shellcheck errors are always high severity. Shellcheck errors are classified by their embedded severity (error/warning = high, info/style = low).
  • Non-strict mode now gates on severity: handleActionlintFindings returns an error when high-severity findings exist, even without --strict.
  • Low-severity findings are reported but don't fail: SC2016:info-level shellcheck findings (like the 6 in the referenced CI run) no longer block compilation.

Severity extraction

Shellcheck messages from actionlint follow the format:

shellcheck reported issue in this script: SC2016:info:4:13: Expressions don't expand in single quotes
                                                  ^^^^
                                                  severity parsed here

extractShellcheckSeverity parses this field; unrecognized formats default to high severity (fail-safe).

In non-strict mode, actionlint now fails only on high-severity errors
(non-shellcheck errors, or shellcheck errors with 'error'/'warning' severity).
Low-severity shellcheck findings (info/style) no longer cause failure.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI requested a review from pelikhan August 6, 2026 13:15
@pelikhan
pelikhan marked this pull request as ready for review August 6, 2026 13:25
Copilot AI balanced review requested due to automatic review settings August 6, 2026 13:25
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Test Quality Sentinel: Analysis complete. Score 100/100. All 3 new tests are design-contract tests with edge-case coverage. No violations detected.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Great work! 🎉 This PR looks solid and ready for review. The fix for actionlint severity classification is well-focused, includes comprehensive tests (91 new test lines), and has a clear description of the problem and solution.

What's working well:

  • ✓ Focused change: addresses the specific issue of non-strict mode ignoring high-severity errors
  • ✓ Good test coverage: actionlint_test.go includes 91 lines of new tests
  • ✓ Clear documentation: explanation includes format details and fail-safe defaults
  • ✓ Well-scoped: changes only affect actionlint-related code

This is ready for maintainer review!

Generated by ✅ Contribution Check · auto · 57.2 AIC · ⊞ 8.7K ·

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

Adds severity-aware actionlint gating so non-strict compilation fails only for high-severity findings.

Changes:

  • Classifies ShellCheck findings by embedded severity.
  • Gates non-strict compilation on high-severity findings.
  • Adds severity parsing and counting tests.
Show a summary per file
File Description
pkg/cli/actionlint.go Implements severity classification and gating.
pkg/cli/actionlint_test.go Tests severity extraction and counting.
eslint-factory/src/rules/require-fetch-response-body-try-catch.test.ts Reformats an existing test case.

Review details

Tip

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

  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread pkg/cli/actionlint.go
Comment on lines +509 to +514
if stdout == "" || strings.TrimSpace(stdout) == "" {
return 0
}
var errors []actionlintError
if err := json.Unmarshal([]byte(stdout), &errors); err != nil {
return 0
Comment thread pkg/cli/actionlint.go
Comment on lines +545 to +558
func extractShellcheckSeverity(message string) string {
// Find the "SC" code prefix
scIdx := strings.Index(message, "SC")
if scIdx < 0 {
return ""
}
// After "SC<digits>:" we expect the severity
rest := message[scIdx:]
parts := strings.SplitN(rest, ":", 3)
if len(parts) < 2 {
return ""
}
return parts[1]
}

@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.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Review: feat: fail compile --actionlint on high severity errors

The overall approach is correct and well-tested. One blocking issue needs to be addressed before merging.

Blocking issue: countHighSeverityErrors silently returns 0 on JSON parse failure, which contradicts the PR's stated fail-safe behaviour. If actionlint outputs non-JSON (e.g. a plain-text error message on tool misconfiguration), the function treats it as "no high-severity errors" and allows compilation to succeed. This is the exact failure mode the PR is trying to prevent.

Non-blocking observations:

  • The severity classification logic (extractShellcheckSeverity) and tests are clean and correct.
  • Defaulting unrecognised shellcheck severity strings to true (high) in isHighSeverityActionlintError correctly applies the fail-safe principle there.
  • The strict vs non-strict refactoring is a clear improvement.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 21.2 AIC · ⊞ 5.3K

Comment thread pkg/cli/actionlint.go
if stdout == "" || strings.TrimSpace(stdout) == "" {
return 0
}
var errors []actionlintError

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.

Silent failure on JSON parse error violates the fail-safe principle

The PR description states "unrecognized formats default to high severity (fail-safe)", but countHighSeverityErrors returns 0 when JSON unmarshalling fails:

if err := json.Unmarshal([]byte(stdout), &errors); err != nil {
    return 0
}

If stdout is non-empty but not valid JSON (e.g. actionlint emits a plain-text error or partial output on failure), this silently classifies all findings as low-severity and lets compilation succeed — the opposite of fail-safe.

Suggested fix:

if err := json.Unmarshal([]byte(stdout), &errors); err != nil {
    actionlintLog.Printf("countHighSeverityErrors: failed to parse actionlint output: %v", err)
    return 1 // fail-safe: treat unparseable output as high severity
}

@copilot please address this.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Blocked: Design Decision Gate - ADR Required

This PR makes significant changes to core business logic (165 new lines in pkg/) but does not have a linked Architecture Decision Record (ADR).

Draft ADR committed: docs/adr/50874-actionlint-severity-based-failure-in-non-strict-mode.md - review and complete it before merging.

This PR cannot merge until an ADR is linked in the PR body.

What to do next:

  1. Review the draft ADR committed to your branch at docs/adr/50874-actionlint-severity-based-failure-in-non-strict-mode.md - it was generated from the PR diff
  2. Complete the missing sections - add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding: ADR: ADR-50874 docs/adr/50874-actionlint-severity-based-failure-in-non-strict-mode.md

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

Why ADRs matter: ADRs create a searchable permanent record of why the codebase looks the way it does. Future contributors and your future self will thank you.

Michael Nygard ADR Format - an ADR must contain: Context, Decision, Alternatives Considered, and Consequences sections. All ADRs are stored in docs/adr/ as Markdown files numbered by PR number.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · sonnet46 · 55.9 AIC · ⊞ 9.6K ·
Comment /review to run again

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

✅ Test Quality Sentinel Report

PR: #50874 — "feat: fail compile --actionlint on high severity errors"
Test Quality Score: 100/100 ⭐ Excellent

Metrics

  • New test functions: 3
  • Design-contract tests: 3/3 (100%)
  • Tests with edge cases: 3/3 (100%)
  • Assertions with context: 12/12
  • Mock violations: 0 ✅
  • Test inflation ratio: 1.23:1 ✅
  • Build tags: ✅ //go:build !integration

New Tests

  1. TestIsHighSeverityActionlintError (lines 522–567)

    • 6 table-driven scenarios covering error/warning/info/style/unparseable severities
    • Directly tests PR's core feature: severity classification logic
    • Grade: ✅ Design test
  2. TestExtractShellcheckSeverity (lines 569–588)

    • 5 scenarios including malformed input handling
    • Isolates severity parsing from shellcheck message format
    • Grade: ✅ Design test
  3. TestCountHighSeverityErrors (lines 590–611)

    • 2 scenarios: mixed high/low, all low severity
    • Integration test for filtering logic
    • Grade: ✅ Design test

Verdict

Quality passes — no violations. The PR adds focused, well-designed unit tests that comprehensively cover the new severity classification feature.

🧪 Test quality analysis by Test Quality Sentinel · haiku45 · 26.6 AIC · ⊞ 7.7K ·
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: 100/100. All 3 new tests are design-contract tests with comprehensive edge-case coverage. 0% implementation tests. No violations detected.

@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.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Verdict: Request changes — one high-severity parsing bug plus a fail-open severity check undermine this PR's own goal.

Themes
  • High: extractShellcheckSeverity uses a bare strings.Index(message, "SC") to locate the shellcheck code prefix, which can match unrelated "SC" substrings inside free-text messages and misclassify severity.
  • Medium: countHighSeverityErrors silently returns 0 on JSON unmarshal failure, so an unparseable actionlint payload is treated as "no high-severity findings" and compile passes — contradicting the stated fail-safe design.
  • Medium: stdout is parsed twice (once in parseAndDisplayActionlintOutput, again in countHighSeverityErrors) — wasted work and a source of future divergence between the two decoders.
  • Medium: The core new behavior (handleActionlintFindings gating non-strict mode on severity) has no direct test; only the lower-level helper functions are tested in isolation.

Grumpy-coder sub-agent findings were reviewed and all four were confirmed as valid on independent inspection of the diff.

🔎 Code quality review by PR Code Quality Reviewer · auto · 59.5 AIC · ⊞ 7.8K
Comment /review to run again

Comment thread pkg/cli/actionlint.go
fileDescription := actionlintFileDescription(lockFiles)

// In non-strict mode, fail only on high-severity errors.
highSeverityCount := countHighSeverityErrors(stdout)

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.

countHighSeverityErrors re-parses the same stdout JSON that parseAndDisplayActionlintOutput already unmarshaled a few lines earlier in the caller — wasted work and a maintenance hazard if the two parsers ever diverge.

💡 Duplicate JSON parsing of the same actionlint output

runActionlintOnFilesWithOptions already calls parseAndDisplayActionlintOutput(runResult.stdout, verbose), which unmarshals stdout into []actionlintError and computes totalErrors/errorsByKind. handleActionlintFindings now performs a second, independent json.Unmarshal of the identical payload via countHighSeverityErrors(stdout).

Besides the redundant allocation/CPU cost on every compile, this creates two separate code paths decoding the same JSON — if actionlintError's tags or unmarshal semantics change in one place but not the other, severity counting and display counting can silently diverge.

Fix: thread the already-parsed []actionlintError slice from parseAndDisplayActionlintOutput through to handleActionlintFindings instead of re-parsing stdout as a raw string.

Comment thread pkg/cli/actionlint.go
return 0
}
var errors []actionlintError
if err := json.Unmarshal([]byte(stdout), &errors); err != 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.

countHighSeverityErrors silently returns 0 on JSON unmarshal failure, so a corrupted/unparseable actionlint payload is treated as "zero high-severity errors" and compile passes — the opposite of the fail-safe design this PR claims to provide.

💡 Parse failures are swallowed instead of surfaced

If json.Unmarshal fails inside countHighSeverityErrors, the function returns 0 with no logging and no propagated error. This is distinct from parseErr, which is already computed by parseAndDisplayActionlintOutput earlier in the call chain and tracked as an IntegrationErrors stat.

In handleActionlintFindings, this means: when parseErr != nil (payload already known to be malformed), highSeverityCount will also be 0 because the second json.Unmarshal in countHighSeverityErrors fails identically — so the function falls through to return nil, silently passing compile even though actionlint genuinely exited with errors that could not be parsed. The doc comment for extractShellcheckSeverity explicitly says unrecognized formats default to high severity as "fail-safe", but this top-level unmarshal failure path does the opposite and fails open.

Fix: reuse the already-computed parseErr/parsed errors from parseAndDisplayActionlintOutput instead of re-parsing, or at minimum log the unmarshal error and treat unparseable output as high severity (consistent with the fail-safe intent stated elsewhere in this diff).

Comment thread pkg/cli/actionlint.go
// Expected format: "shellcheck reported issue in this script: SC<code>:<severity>:<line>:<col>: <message>"
func extractShellcheckSeverity(message string) string {
// Find the "SC" code prefix
scIdx := strings.Index(message, "SC")

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.

strings.Index(message, "SC") can match the literal substring "SC" anywhere in the free-text shellcheck message, not just the intended SC<code>: prefix — misclassifying severity.

💡 Fragile substring search for the shellcheck code prefix

The expected format is shellcheck reported issue in this script: SC2086:error:4:13: msg, but strings.Index(message, "SC") finds the first occurrence of the literal two-character substring "SC" anywhere in the message — including inside arbitrary shellcheck wording that happens to contain "SC" (e.g. words like "SCRIPT", "DESCRIPTION", "DISCARD", or even a message that legitimately starts with different casing/wording before the code). If any such text precedes the real SC<code> token, scIdx points at the wrong location and the subsequent strings.SplitN(rest, ":", 3) extracts garbage as the "severity", silently misclassifying a finding as high or low severity.

Fix: parse severity with an anchored regex (e.g. regexp.MustCompile(SC\d+:(\w+):)) or split on the known prefix "in this script: " first, rather than a bare substring search for "SC".

Comment thread pkg/cli/actionlint.go

func handleActionlintFindings(strict bool, lockFiles []string, totalErrors int, parseErr error) error {
if !strict {
func handleActionlintFindings(strict bool, lockFiles []string, totalErrors int, parseErr error, stdout string) error {

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.

handleActionlintFindings is the function whose behavior actually changed (non-strict mode now fails on high severity), yet no test exercises this integration path end-to-end.

💡 Missing integration test for the core new behavior

The new tests (TestIsHighSeverityActionlintError, TestExtractShellcheckSeverity, TestCountHighSeverityErrors) only cover the lower-level helpers in isolation. handleActionlintFindings itself — the function that decides whether compile --actionlint fails in non-strict mode — has no direct test verifying:

  • it returns a non-nil error when stdout contains high-severity findings and strict=false
  • it returns nil when stdout contains only low-severity (info/style) findings and strict=false
  • the strict-mode branch is unaffected by the new stdout parameter
  • behavior when stdout is unparseable in non-strict mode (see the fail-open concern noted elsewhere)

This is the actual regression surface the PR is meant to fix (per the linked CI run); testing only the helper functions leaves the wiring/branch logic unverified.

@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.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — requesting changes on two correctness issues.

📋 Key Themes & Highlights

Key Themes

  • Dual JSON parse: countHighSeverityErrors re-decodes stdout that parseAndDisplayActionlintOutput already decoded. The decoded []actionlintError slice should be threaded through the call chain instead.
  • Silent fail-open on parse error: when the second parse in countHighSeverityErrors fails it returns 0, silently passing the gate with no visible warning. The existing parseErr path already handles this correctly if the refactor above is adopted.
  • Test gap: the intentional fail-open behaviour on malformed JSON in countHighSeverityErrors is not covered by a test.

Positive Highlights

  • ✅ Fail-safe default (unknown message format → high severity) is a solid design choice
  • ✅ Well-structured unit tests for extractShellcheckSeverity and isHighSeverityActionlintError
  • ✅ Clear PR description with the shellcheck format documented inline

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

Comment thread pkg/cli/actionlint.go
fileDescription := actionlintFileDescription(lockFiles)

// In non-strict mode, fail only on high-severity errors.
highSeverityCount := countHighSeverityErrors(stdout)

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.

[/diagnosing-bugs] countHighSeverityErrors re-parses stdout JSON a second time, even though parseAndDisplayActionlintOutput already decoded the same data into []actionlintError above. This means two unmarshal passes on the same string, and the count can silently return 0 if the second parse fails while the first succeeded.

💡 Suggested refactor

Pass the already-decoded slice through the call chain instead of stdout:

// handleActionlintFindings(strict, lockFiles, totalErrors, parseErr, errors []actionlintError)
highSeverityCount := countHighSeverityErrors(errors)

This removes the dual-parse and aligns with the existing totalErrors pattern.

@copilot please address this.

Comment thread pkg/cli/actionlint.go
return fmt.Errorf("actionlint found %d high severity error(s) in %s", highSeverityCount, fileDescription)
}

if parseErr != 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.

[/diagnosing-bugs] When countHighSeverityErrors returns 0 due to a JSON parse failure, the function silently returns nil — treating a parse error as "no high-severity errors". The parseErr check below (line 461) logs a debug message but still returns nil, so a real failure in the output format would silently pass the gate.

💡 Suggested fix

Consider promoting the parse-failure path to a user-visible warning before returning nil, or — if the refactor above is adopted — rely on the existing parseErr from parseAndDisplayActionlintOutput which already surfaces this via a warning message to stderr.

@copilot please address this.

// All low severity
input2 := `[
{"message":"shellcheck reported issue in this script: SC2016:info:4:13: msg","filepath":"a.yml","line":1,"column":1,"kind":"shellcheck"},
{"message":"shellcheck reported issue in this script: SC2034:style:1:1: msg","filepath":"b.yml","line":2,"column":1,"kind":"shellcheck"}

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.

[/tdd] TestCountHighSeverityErrors doesn't cover the JSON-parse-failure path, which is the fail-safe that silently returns 0. Without a test for malformed input, it's easy for someone to inadvertently change the fail-safe behaviour (e.g. to return a non-zero count) without a failing test.

💡 Suggested test case
// malformed JSON returns 0 (fail-open)
got3 := countHighSeverityErrors(`not valid json`)
if got3 != 0 {
    t.Errorf("countHighSeverityErrors() = %d, want 0 for malformed input", got3)
}

A brief comment explaining the intentional fail-open behaviour here would also help future readers.

@copilot please address this.

@pelikhan

pelikhan commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@pelikhan
pelikhan merged commit 8b346a6 into main Aug 6, 2026
96 of 111 checks passed
@pelikhan
pelikhan deleted the copilot/ensure-actionlint-fails-on-errors branch August 6, 2026 14:54
Copilot stopped work on behalf of pelikhan due to an error August 6, 2026 14:55
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.86.0

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants