From 8ab022f19423175b09acd8acf29ba39393b3714c Mon Sep 17 00:00:00 2001 From: Sputnik-MAC Date: Thu, 5 Mar 2026 23:10:37 +0700 Subject: [PATCH 1/2] feat(cmd): differentiate exit codes for compliance vs runtime errors (closes #61) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a dedicated ComplianceError type so that callers can distinguish between two different failure modes: Exit 0 – analysis passed (compliance >= threshold) Exit 1 – compliance failure (compliance < threshold) Exit 2 – runtime / configuration error Previously both compliance failures and internal errors returned exit code 1, making it impossible for CI systems to handle them differently. Changes: - cmd/errors.go: new ComplianceError struct - cmd/analyze.go: return *ComplianceError when compliance < threshold; update the 'Exit codes' section in the command help text - cmd/root.go: use errors.As to map ComplianceError → exit 1, all other errors → exit 2 --- cmd/analyze.go | 5 +++-- cmd/errors.go | 21 +++++++++++++++++++++ cmd/root.go | 7 ++++++- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 cmd/errors.go diff --git a/cmd/analyze.go b/cmd/analyze.go index f3ceb43..c76900b 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -69,7 +69,8 @@ Optional flags: Exit codes: 0 Analysis passed (compliance >= threshold) - 1 Analysis failed (compliance < threshold or error occurred) + 1 Compliance failure (compliance < threshold) + 2 Runtime error (configuration error, network failure, missing token, etc.) Examples: # Set token via environment variable @@ -403,7 +404,7 @@ func runAnalyze(cmd *cobra.Command, args []string) error { // Check compliance against threshold if compliance < threshold { - return fmt.Errorf("compliance %.1f%% is below threshold %.1f%%", compliance, threshold) + return &ComplianceError{Compliance: compliance, Threshold: threshold} } return nil diff --git a/cmd/errors.go b/cmd/errors.go new file mode 100644 index 0000000..d439caa --- /dev/null +++ b/cmd/errors.go @@ -0,0 +1,21 @@ +package cmd + +import "fmt" + +// ComplianceError is returned when analysis completes successfully but the +// measured compliance score falls below the required threshold. +// +// It is distinct from a generic runtime error so that callers (e.g. Execute) +// can map it to a dedicated exit code: +// +// 0 – analysis passed (compliance ≥ threshold) +// 1 – compliance failure (compliance < threshold) +// 2 – runtime / configuration error +type ComplianceError struct { + Compliance float64 + Threshold float64 +} + +func (e *ComplianceError) Error() string { + return fmt.Sprintf("compliance %.1f%% is below threshold %.1f%%", e.Compliance, e.Threshold) +} diff --git a/cmd/root.go b/cmd/root.go index 6419569..3b5fe9a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "os" "time" @@ -48,7 +49,11 @@ func Execute() { } if err != nil { - os.Exit(1) + var complianceErr *ComplianceError + if errors.As(err, &complianceErr) { + os.Exit(1) + } + os.Exit(2) } } From 5135081b5f55ad243a5b2fa7e762dfe279e891c1 Mon Sep 17 00:00:00 2001 From: Joseph Moukarzel Date: Fri, 13 Mar 2026 14:43:55 +0100 Subject: [PATCH 2/2] docs(readme): Update readme to reflect exit code changes --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index db2a987..d183f38 100644 --- a/README.md +++ b/README.md @@ -935,10 +935,11 @@ export PLUMBER_NO_UPDATE_CHECK=1 ### Exit Codes -| Code | Meaning | -|------|---------| -| `0` | Compliance ≥ threshold | -| `1` | Compliance < threshold or error | +| Exit Code | Meaning | +|-----------|----------| +| `0` | Analysis passed (compliance ≥ threshold) | +| `1` | Compliance failure (compliance < threshold) | +| `2` | Runtime error (config error, network failure, missing token, etc.) | ### `plumber config generate`