From 2b0062a5911a4a41b5a2c1bce46be214c87a6fe4 Mon Sep 17 00:00:00 2001 From: Benjamin Bengfort Date: Sat, 21 Mar 2026 14:11:37 -0500 Subject: [PATCH 1/2] Improve Invalid Configuration Errors --- errors.go | 21 ++++-- errors/config.go | 73 ++++++++++++++++++++ errors/config_test.go | 87 ++++++++++++++++++++++++ errors/errors.go | 106 +++++++++++++++++++---------- errors/errors_test.go | 137 ++++++++++++++++++++++++++++---------- errors/parse.go | 26 ++++++++ errors/parse_test.go | 24 +++++++ errors_test.go | 54 +++------------ validate/required.go | 5 +- validate/validate.go | 9 +-- validate/validate_test.go | 2 +- 11 files changed, 408 insertions(+), 136 deletions(-) create mode 100644 errors/config.go create mode 100644 errors/config_test.go create mode 100644 errors/parse.go create mode 100644 errors/parse_test.go diff --git a/errors.go b/errors.go index 91dc615..5e75e40 100644 --- a/errors.go +++ b/errors.go @@ -21,17 +21,26 @@ func IsParseError(err error) bool { return errors.As(err, &target) } -// Extract a validation error from an error if it is one. -func ValidationError(err error) (*confireErrors.ValidationError, bool) { - target := &confireErrors.ValidationError{} +// Extract a configuration error from an error if it is one. +func InvalidConfig(err error) (*confireErrors.InvalidConfig, bool) { + target := &confireErrors.InvalidConfig{} if ok := errors.As(err, &target); ok { return target, true } return nil, false } -// Returns true if the underlying error is a validation error. -func IsValidationError(err error) bool { - target := &confireErrors.ValidationError{} +// Returns true if the underlying error is a configuration error. +func IsInvalidConfig(err error) bool { + target := &confireErrors.InvalidConfig{} return errors.As(err, &target) } + +// Bringing in the errors from the errors package for convenience. +var ( + Required = confireErrors.Required + Invalid = confireErrors.Invalid + Parse = confireErrors.Parse + Wrap = confireErrors.Wrap + Join = confireErrors.Join +) diff --git a/errors/config.go b/errors/config.go new file mode 100644 index 0000000..94c8e1e --- /dev/null +++ b/errors/config.go @@ -0,0 +1,73 @@ +package errors + +import ( + "errors" + "fmt" +) + +func Required(conf, field string) *InvalidConfig { + return &InvalidConfig{ + conf: conf, + field: field, + issue: "is required but not set", + err: ErrMissingRequired, + } +} + +func Invalid(conf, field, issue string, args ...any) *InvalidConfig { + return &InvalidConfig{ + conf: conf, + field: field, + issue: fmt.Sprintf(issue, args...), + } +} + +func Parse(conf, field string, err error) *InvalidConfig { + return &InvalidConfig{ + conf: conf, + field: field, + issue: fmt.Sprintf("could not parse value: %s", err.Error()), + err: err, + } +} + +func Wrap(conf, field, issue string, err error, args ...any) *InvalidConfig { + return &InvalidConfig{ + conf: conf, + field: field, + issue: fmt.Sprintf(issue, args...), + err: err, + } +} + +// Invalid is a field-specific configuration validation error and is returned either by +// field specification validation or by the user in a custom Validate() method. +type InvalidConfig struct { + conf string + field string + issue string + err error +} + +func (e *InvalidConfig) Error() string { + field := e.field + if e.conf != "" { + field = e.conf + "." + e.field + } + return fmt.Sprintf("invalid configuration: %s %s", field, e.issue) +} + +func (e *InvalidConfig) Field() string { + if e.conf != "" { + return e.conf + "." + e.field + } + return e.field +} + +func (e *InvalidConfig) Is(target error) bool { + return errors.Is(e.err, target) +} + +func (e *InvalidConfig) Unwrap() error { + return e.err +} diff --git a/errors/config_test.go b/errors/config_test.go new file mode 100644 index 0000000..839d3ef --- /dev/null +++ b/errors/config_test.go @@ -0,0 +1,87 @@ +package errors + +import ( + "errors" + "testing" + + "go.rtnl.ai/confire/assert" +) + +func TestRequired(t *testing.T) { + err := Required("", "bind_addr") + assert.Equals(t, "invalid configuration: bind_addr is required but not set", err.Error()) + assert.Assert(t, err.Is(ErrMissingRequired), "required should wrap a missing required field error") + assert.Equals(t, ErrMissingRequired, err.Unwrap()) +} + +func TestInvalid(t *testing.T) { + err := Invalid("", "mode", "invalid mode %q", "foo") + assert.Equals(t, "invalid configuration: mode invalid mode \"foo\"", err.Error()) + assert.Equals(t, nil, err.Unwrap()) +} + +func TestParse(t *testing.T) { + err := Parse("", "bind_addr", errors.New("invalid bind address")) + assert.Equals(t, "invalid configuration: bind_addr could not parse value: invalid bind address", err.Error()) + assert.Equals(t, errors.New("invalid bind address"), err.Unwrap()) +} + +func TestWrap(t *testing.T) { + err := Wrap("", "bind_addr", "invalid bind address %q", errors.New("invalid bind address"), "foo") + assert.Equals(t, "invalid configuration: bind_addr invalid bind address \"foo\"", err.Error()) + assert.Equals(t, errors.New("invalid bind address"), err.Unwrap()) +} + +func TestInvalidConfig(t *testing.T) { + testCases := []struct { + conf string + field string + issue string + err error + errstr string + fieldstr string + }{ + { + conf: "", + field: "bind_addr", + issue: "is required but not set", + errstr: "invalid configuration: bind_addr is required but not set", + fieldstr: "bind_addr", + }, + { + conf: "", + field: "bind_addr", + issue: "is required but not set", + err: errors.New("required field is zero valued"), + errstr: "invalid configuration: bind_addr is required but not set", + fieldstr: "bind_addr", + }, + { + conf: "telemetry", + field: "service_name", + issue: "cannot have spaces or start with a number", + errstr: "invalid configuration: telemetry.service_name cannot have spaces or start with a number", + fieldstr: "telemetry.service_name", + }, + { + conf: "telemetry", + field: "service_name", + issue: "cannot have spaces or start with a number", + err: errors.New("invalid service name"), + errstr: "invalid configuration: telemetry.service_name cannot have spaces or start with a number", + fieldstr: "telemetry.service_name", + }, + } + + for _, tc := range testCases { + err := Wrap(tc.conf, tc.field, tc.issue, tc.err) + assert.Equals(t, tc.errstr, err.Error()) + assert.Equals(t, tc.fieldstr, err.Field()) + if tc.err != nil { + assert.Assert(t, err.Is(tc.err), "wrap should wrap the error") + assert.Equals(t, tc.err, err.Unwrap()) + } else { + assert.Equals(t, nil, err.Unwrap()) + } + } +} diff --git a/errors/errors.go b/errors/errors.go index 4d81f80..4d573e3 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -11,57 +11,89 @@ var ( ErrNotAStruct = errors.New("cannot wrap a non-struct type") ErrNotExported = errors.New("field is not exported") ErrNotSettable = errors.New("field is not settable") - ErrMissingRequiredField = errors.New("required field is zero valued") + ErrMissingRequired = errors.New("required field is zero valued") ) -type ParseError struct { - Source string - Field string - Type string - Value string - Err error -} - -func (e *ParseError) Error() string { - return fmt.Sprintf("confire: could not parse %[2]s from %[1]s: converting %[4]q to type %[3]s: %[5]s", e.Source, e.Field, e.Type, e.Value, e.Err) -} +type ValidationErrors []*InvalidConfig -func (e *ParseError) Is(target error) bool { - return errors.Is(e.Err, target) -} +func (e ValidationErrors) Error() string { + if len(e) == 1 { + return e[0].Error() + } -func (e *ParseError) Unwrap() error { - return e.Err + sb := strings.Builder{} + sb.WriteString(fmt.Sprintf("%d validation errors occurred:", len(e))) + for _, err := range e { + sb.WriteString(fmt.Sprintf("\n - %s", err.Error())) + } + return sb.String() } -type ValidationError struct { - Source string - Err error +func (e ValidationErrors) Is(target error) bool { + for _, err := range e { + if errors.Is(err, target) { + return true + } + } + return false } -func (e *ValidationError) Error() string { - return fmt.Sprintf("invalid configuration: %s", e.Err) +func (e ValidationErrors) Contains(target error) bool { + return e.Is(target) } -func (e *ValidationError) Is(target error) bool { - return errors.Is(e.Err, target) -} +func Join(err error, errs ...error) error { + var ( + verrs ValidationErrors + isverr bool + ) -func (e *ValidationError) Unwrap() error { - return e.Err -} + // If the first error is not nil and it is not ValidationErrors then use the + // regular errors.Join function. If it is a ValidationError then continue. If the + // first error is nil, then create new ValidationErrors. + if err != nil { + // If the error is an InvalidConfig error then create a new ValidationErrors + if cerr, ok := err.(*InvalidConfig); ok { + verrs = ValidationErrors{cerr} + } else { + // If the error is ValidationErrors then flatten it into the current ValidationErrors. + verrs, isverr = err.(ValidationErrors) + if !isverr { + errs = append([]error{err}, errs...) + return errors.Join(errs...) + } + } + } else { + verrs = make(ValidationErrors, 0, len(errs)) + } -type ValidationErrors []*ValidationError + // Loop through the remaining errors and append them to the ValidationErrors if + // they are not nil and they are InvalidConfig errors. If they are not InvalidConfig + // errors then use the regular errors.Join function. + for _, err := range errs { + if err != nil { + // If the error is ValidationErrors then flatten it into the current ValidationErrors. + if errs, ok := err.(ValidationErrors); ok { + verrs = append(verrs, errs...) + continue + } -func (e ValidationErrors) Error() string { - if len(e) == 1 { - return e[0].Error() + cerr, iscerr := err.(*InvalidConfig) + if !iscerr { + errs = append([]error{err}, errs...) + return errors.Join(errs...) + } + verrs = append(verrs, cerr) + } } - sb := strings.Builder{} - sb.WriteString(fmt.Sprintf("%d validation errors occurred:", len(e))) - for _, err := range e { - sb.WriteString(fmt.Sprintf("\n - %s", err.Error())) + // If the ValidationErrors is empty, then return nil. + switch len(verrs) { + case 0: + return nil + case 1: + return verrs[0] + default: + return verrs } - return sb.String() } diff --git a/errors/errors_test.go b/errors/errors_test.go index a59d7e2..ebcc5a0 100644 --- a/errors/errors_test.go +++ b/errors/errors_test.go @@ -8,47 +8,112 @@ import ( . "go.rtnl.ai/confire/errors" ) -func TestParseError(t *testing.T) { - werr := errors.New("something bad happened") - err := &ParseError{ - Source: "source", - Field: "field", - Type: "foo", - Value: "value", - Err: werr, - } - - assert.Assert(t, err.Is(werr), "parse error should wrap an error") - assert.Equals(t, werr, err.Unwrap()) - assert.Equals(t, "confire: could not parse field from source: converting \"value\" to type foo: something bad happened", err.Error()) - - // This is to appease the linter for historical reasons and can probably be removed - // if you're reading this; sorry it got left here for so long. - assert.Ok(t, nil) -} +func TestValidationErrors(t *testing.T) { + errs := make(ValidationErrors, 0, 3) + assert.Equals(t, "0 validation errors occurred:", errs.Error()) + + errs = append(errs, Required("", "foo")) + assert.Equals(t, "invalid configuration: foo is required but not set", errs.Error()) + assert.True(t, errs.Contains(ErrMissingRequired)) + assert.False(t, errs.Contains(ErrNotAStruct)) -func TestValidationError(t *testing.T) { - werr := errors.New("that's not right") - err := &ValidationError{ - Source: "source", - Err: werr, - } + errs = append(errs, Wrap("", "colors", "at least one color must be specified", errors.New("at least one color should be specified"))) + assert.Equals(t, "2 validation errors occurred:\n - invalid configuration: foo is required but not set\n - invalid configuration: colors at least one color must be specified", errs.Error()) - assert.Assert(t, err.Is(werr), "parse error should wrap an error") - assert.Equals(t, werr, err.Unwrap()) - assert.Equals(t, "invalid configuration: that's not right", err.Error()) + invalid := Invalid("", "port", "port number out of range") + errs = append(errs, invalid) + assert.Equals(t, "3 validation errors occurred:\n - invalid configuration: foo is required but not set\n - invalid configuration: colors at least one color must be specified\n - invalid configuration: port port number out of range", errs.Error()) + assert.True(t, errs.Contains(invalid)) + assert.False(t, errs.Contains(ErrNotAStruct)) } -func TestValidationErrors(t *testing.T) { - errs := make(ValidationErrors, 0, 3) - assert.Equals(t, "0 validation errors occurred:", errs.Error()) +func TestJoin(t *testing.T) { + t.Run("Nil", func(t *testing.T) { + assert.Equals(t, nil, Join(nil)) + assert.Equals(t, nil, Join(nil, nil)) + assert.Equals(t, nil, Join(nil, nil, nil)) + assert.Equals(t, nil, Join(nil, nil, nil, nil)) + assert.Equals(t, nil, Join(ValidationErrors{}, nil)) + assert.Equals(t, nil, Join(ValidationErrors{}, nil, nil)) + assert.Equals(t, nil, Join(ValidationErrors{}, nil, nil, nil)) + assert.Equals(t, nil, Join(nil, ValidationErrors{})) + assert.Equals(t, nil, Join(nil, ValidationErrors{}, nil)) + assert.Equals(t, nil, Join(nil, ValidationErrors{}, nil, ValidationErrors{})) + assert.Equals(t, nil, Join(ValidationErrors{}, nil, ValidationErrors{}, nil)) + assert.Equals(t, nil, Join(ValidationErrors{}, ValidationErrors{}, ValidationErrors{}, ValidationErrors{})) + }) + + t.Run("Single", func(t *testing.T) { + required := Required("", "foo") + assert.Equals(t, required, Join(required)) + assert.Equals(t, required, Join(required, nil)) + assert.Equals(t, required, Join(nil, required)) + assert.Equals(t, required, Join(ValidationErrors{}, required)) + assert.Equals(t, required, Join(required, nil, nil)) + assert.Equals(t, required, Join(nil, required, nil)) + assert.Equals(t, required, Join(ValidationErrors{}, nil, required, nil, nil)) + assert.Equals(t, required, Join(ValidationErrors{}, nil, nil, nil, required)) + }) + + t.Run("Multiple", func(t *testing.T) { + required := Required("", "foo") + badport := Invalid("", "port", "port number out of range") + parse := Parse("", "bind_addr", errors.New("invalid bind address")) + colors := Wrap("", "colors", "at least one color must be specified", errors.New("at least one color should be specified")) + + testCases := []struct { + err error + errs []error + expectedLength int + expectedContains []error + }{ + {required, []error{badport}, 2, []error{required, badport}}, + {required, []error{badport, parse}, 3, []error{required, badport, parse}}, + {required, []error{badport, parse, colors}, 4, []error{required, badport, parse, colors}}, + {nil, []error{required, badport, parse, colors}, 4, []error{required, badport, parse, colors}}, + {ValidationErrors{}, []error{required, badport, parse, colors}, 4, []error{required, badport, parse, colors}}, + {nil, []error{nil, required, nil, badport, nil, nil}, 2, []error{required, badport}}, + {ValidationErrors{}, []error{nil, nil, badport, nil, colors}, 2, []error{badport, colors}}, + {nil, []error{ValidationErrors{required, badport, parse, colors}}, 4, []error{required, badport, parse, colors}}, + {ValidationErrors{required, badport}, []error{parse, colors}, 4, []error{required, badport, parse, colors}}, + {ValidationErrors{required, badport}, []error{ValidationErrors{parse, colors}}, 4, []error{required, badport, parse, colors}}, + {ValidationErrors{required}, []error{ValidationErrors{badport}, ValidationErrors{parse}, ValidationErrors{colors}}, 4, []error{required, badport, parse, colors}}, + } + + for _, tc := range testCases { + err := Join(tc.err, tc.errs...) + verrs, isverr := err.(ValidationErrors) + assert.True(t, isverr) + assert.Equals(t, tc.expectedLength, len(verrs)) + for _, err := range tc.expectedContains { + assert.True(t, verrs.Contains(err)) + } + } + }) - errs = append(errs, &ValidationError{Source: "Name", Err: ErrMissingRequiredField}) - assert.Equals(t, "invalid configuration: required field is zero valued", errs.Error()) + t.Run("UglyDuck", func(t *testing.T) { + other := errors.New("not a validation error") + required := Required("", "foo") + badport := Invalid("", "port", "port number out of range") - errs = append(errs, &ValidationError{Source: "Colors", Err: errors.New("at least one color should be specified")}) - assert.Equals(t, "2 validation errors occurred:\n - invalid configuration: required field is zero valued\n - invalid configuration: at least one color should be specified", errs.Error()) + testCases := []struct { + err error + errs []error + expectedIs []error + }{ + {other, []error{required, badport}, []error{other, required, badport}}, + {nil, []error{other, required, badport}, []error{other, required, badport}}, + {ValidationErrors{}, []error{required, other, badport}, []error{other, required, badport}}, + {nil, []error{required, badport, other}, []error{other, required, badport}}, + } - errs = append(errs, &ValidationError{Source: "Port", Err: errors.New("port number out of range")}) - assert.Equals(t, "3 validation errors occurred:\n - invalid configuration: required field is zero valued\n - invalid configuration: at least one color should be specified\n - invalid configuration: port number out of range", errs.Error()) + for _, tc := range testCases { + errs := Join(tc.err, tc.errs...) + _, isverr := errs.(ValidationErrors) + assert.False(t, isverr) + for _, err := range tc.expectedIs { + assert.True(t, errors.Is(errs, err)) + } + } + }) } diff --git a/errors/parse.go b/errors/parse.go new file mode 100644 index 0000000..4302a9f --- /dev/null +++ b/errors/parse.go @@ -0,0 +1,26 @@ +package errors + +import ( + "errors" + "fmt" +) + +type ParseError struct { + Source string + Field string + Type string + Value string + Err error +} + +func (e *ParseError) Error() string { + return fmt.Sprintf("confire: could not parse %[2]s from %[1]s: converting %[4]q to type %[3]s: %[5]s", e.Source, e.Field, e.Type, e.Value, e.Err) +} + +func (e *ParseError) Is(target error) bool { + return errors.Is(e.Err, target) +} + +func (e *ParseError) Unwrap() error { + return e.Err +} diff --git a/errors/parse_test.go b/errors/parse_test.go new file mode 100644 index 0000000..2ee540b --- /dev/null +++ b/errors/parse_test.go @@ -0,0 +1,24 @@ +package errors_test + +import ( + "errors" + "testing" + + "go.rtnl.ai/confire/assert" + . "go.rtnl.ai/confire/errors" +) + +func TestParseError(t *testing.T) { + werr := errors.New("something bad happened") + err := &ParseError{ + Source: "source", + Field: "field", + Type: "foo", + Value: "value", + Err: werr, + } + + assert.Assert(t, err.Is(werr), "parse error should wrap an error") + assert.Equals(t, werr, err.Unwrap()) + assert.Equals(t, "confire: could not parse field from source: converting \"value\" to type foo: something bad happened", err.Error()) +} diff --git a/errors_test.go b/errors_test.go index a6b91e7..85eeb7b 100644 --- a/errors_test.go +++ b/errors_test.go @@ -15,7 +15,10 @@ func TestParserError(t *testing.T) { }{ {errors.ErrInvalidSpecification, false}, {&errors.ParseError{Source: "a", Field: "b", Type: "c", Value: "d", Err: errors.ErrNotAStruct}, true}, - {&errors.ValidationError{Source: "foo", Err: errors.ErrNotExported}, false}, + {errors.Required("", "foo"), false}, + {errors.Invalid("", "foo", "bar"), false}, + {errors.Parse("", "foo", errors.ErrNotExported), false}, + {errors.Wrap("", "foo", "bar", errors.ErrNotSettable, "qux"), false}, } for _, tc := range testCases { @@ -42,54 +45,13 @@ func TestIsParserError(t *testing.T) { {errors.ErrNotSettable, assert.False}, {&errors.ParseError{}, assert.True}, {&errors.ParseError{Source: "a", Field: "b", Type: "c", Value: "d", Err: errors.ErrNotAStruct}, assert.True}, - {&errors.ValidationError{}, assert.False}, - {&errors.ValidationError{Source: "foo", Err: errors.ErrNotExported}, assert.False}, + {errors.Required("", "foo"), assert.False}, + {errors.Invalid("", "foo", "bar"), assert.False}, + {errors.Parse("", "foo", errors.ErrNotExported), assert.False}, + {errors.Wrap("", "foo", "bar", errors.ErrNotSettable, "qux"), assert.False}, } for _, tc := range testCases { tc.assert(t, confire.IsParseError(tc.err)) } } - -func TestValidationError(t *testing.T) { - testCases := []struct { - err error - ok bool - }{ - {errors.ErrInvalidSpecification, false}, - {&errors.ParseError{Source: "a", Field: "b", Type: "c", Value: "d", Err: errors.ErrNotAStruct}, false}, - {&errors.ValidationError{Source: "foo", Err: errors.ErrNotExported}, true}, - } - - for _, tc := range testCases { - target, ok := confire.ValidationError(tc.err) - if tc.ok { - assert.True(t, ok) - assert.Assert(t, target != nil, "expected target to be not nil") - } else { - assert.False(t, ok) - assert.Assert(t, target == nil, "expected target to be nil") - } - - } -} - -func TestIsValidationError(t *testing.T) { - testCases := []struct { - err error - assert assert.BoolAssertion - }{ - {errors.ErrInvalidSpecification, assert.False}, - {errors.ErrNotAStruct, assert.False}, - {errors.ErrNotExported, assert.False}, - {errors.ErrNotSettable, assert.False}, - {&errors.ParseError{}, assert.False}, - {&errors.ParseError{Source: "a", Field: "b", Type: "c", Value: "d", Err: errors.ErrNotAStruct}, assert.False}, - {&errors.ValidationError{}, assert.True}, - {&errors.ValidationError{Source: "foo", Err: errors.ErrNotExported}, assert.True}, - } - - for _, tc := range testCases { - tc.assert(t, confire.IsValidationError(tc.err)) - } -} diff --git a/validate/required.go b/validate/required.go index 6de2cb9..9851bfa 100644 --- a/validate/required.go +++ b/validate/required.go @@ -16,10 +16,7 @@ type required struct { func (r required) Validate() error { if r.field.IsZero() { - return &errors.ValidationError{ - Source: r.field.Name(), - Err: errors.ErrMissingRequiredField, - } + return errors.Required("", r.field.Name()) } return nil } diff --git a/validate/validate.go b/validate/validate.go index 12088d2..8a9967b 100644 --- a/validate/validate.go +++ b/validate/validate.go @@ -160,13 +160,10 @@ func ignoreValidation(s string) bool { return false } -func asValidationError(err error, source string) *errors.ValidationError { - target := &errors.ValidationError{} +func asValidationError(err error, source string) *errors.InvalidConfig { + target := &errors.InvalidConfig{} if goerrors.As(err, &target) { return target } - return &errors.ValidationError{ - Source: source, - Err: err, - } + return errors.Wrap("", source, err.Error(), err) } diff --git a/validate/validate_test.go b/validate/validate_test.go index 89a8ffa..05c9791 100644 --- a/validate/validate_test.go +++ b/validate/validate_test.go @@ -66,7 +66,7 @@ func TestRequired(t *testing.T) { err = validate.Validate(partial) assert.Assert(t, err != nil, "expected a validation error to have occurred") - var single *confireErrors.ValidationError + var single *confireErrors.InvalidConfig ok = errors.As(err, &single) assert.True(t, ok) From 21c704ba1d3672ca0ff2e0ef31891c10350d99b3 Mon Sep 17 00:00:00 2001 From: Benjamin Bengfort Date: Sat, 21 Mar 2026 14:16:20 -0500 Subject: [PATCH 2/2] advance go versions --- .github/workflows/test.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 46c125f..7d1ed06 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -20,17 +20,17 @@ jobs: working-directory: ${{ env.GOPATH }}/src/go.rtnl.ai/confire steps: - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v6 with: - go-version: 1.19 + go-version: 1.26 - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: path: ${{ env.GOPATH }}/src/go.rtnl.ai/confire - name: Install Staticcheck - run: go install honnef.co/go/tools/cmd/staticcheck@2023.1.3 + run: go install honnef.co/go/tools/cmd/staticcheck@2026.1 - name: Lint Go Code run: staticcheck ./... @@ -41,7 +41,7 @@ jobs: strategy: fail-fast: true matrix: - go-version: [1.18.x, 1.19.x, 1.20.x] + go-version: [1.20.x, 1.25.x, 1.26.x] env: GOPATH: ${{ github.workspace }}/go GOBIN: ${{ github.workspace }}/go/bin @@ -50,12 +50,12 @@ jobs: working-directory: ${{ env.GOPATH }}/src/go.rtnl.ai/confire steps: - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} - name: Cache Speedup - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-go- - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: path: ${{ env.GOPATH }}/src/go.rtnl.ai/confire