From 92b7fa0b54e0840e0620d2ef72623235fd5c5f53 Mon Sep 17 00:00:00 2001 From: Reda Laanait Date: Sun, 29 Mar 2026 10:09:53 +0100 Subject: [PATCH 1/5] feat: updates --- mask/email.go | 15 +++++++++++++-- scan.go | 7 ++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/mask/email.go b/mask/email.go index 990b238..57edfb2 100644 --- a/mask/email.go +++ b/mask/email.go @@ -8,7 +8,8 @@ import ( ) type EmailConfig struct { - MaskDomain bool // default false + MaskDomain bool // default false + KeepFirstAndLastChar bool // default false } func Email(email string, opts ...func(*Config[EmailConfig])) (string, error) { @@ -22,7 +23,17 @@ func Email(email string, opts ...func(*Config[EmailConfig])) (string, error) { return "", errors.New("invalid email format") } - local := strings.Repeat(string([]rune{cfg.Symbol}), len(parts[0])) + var local string + if cfg.Kind.KeepFirstAndLastChar && len(parts[0]) > 2 { + // Keep first and last character, mask the rest + firstChar := string([]rune(parts[0])[0]) + lastChar := string([]rune(parts[0])[len([]rune(parts[0]))-1]) + middle := strings.Repeat(string([]rune{cfg.Symbol}), len([]rune(parts[0]))-2) + local = firstChar + middle + lastChar + } else { + // Mask the entire local part + local = strings.Repeat(string([]rune{cfg.Symbol}), len(parts[0])) + } domain := parts[1] if cfg.Kind.MaskDomain { diff --git a/scan.go b/scan.go index d2d49fe..f6ad0c5 100644 --- a/scan.go +++ b/scan.go @@ -269,7 +269,12 @@ func (s sensitiveStruct) Replace(fn ReplaceFunc) error { elem := reflect.Indirect(v) if ssField.isData { - val := elem.String() + var val string + if elem.Type().ConvertibleTo(stringType) { + val = elem.Convert(stringType).String() + } else { + val = elem.String() + } newVal, err = fn(FieldReplace{ SubjectID: s.subjectID, From 1ad8a3078346eb1bee4f5fe71e4ed6d6fe2f1a30 Mon Sep 17 00:00:00 2001 From: Reda Laanait Date: Thu, 28 May 2026 10:39:02 +0100 Subject: [PATCH 2/5] fix: . --- README.md | 6 +-- doc.go | 6 +-- mask.go | 23 ++++++--- mask/email.go | 17 +++--- mask/fullname.go | 13 ++--- mask/fullname_test.go | 2 +- mask/mask.go | 7 +-- mask_test.go | 44 +++++++++++++++- redact.go | 2 +- scan.go | 117 +++++++++++++++++++++++++++++++++--------- scan_test.go | 51 ++++++++++++++++++ tag.go | 2 +- 12 files changed, 231 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 73d152e..8382224 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ import ( ... -var defaultMask := func(val string) (masked string, err error) { +defaultMask := func(val string) (masked string, err error) { // TODO implement 'be_nrn' mask behavior here masked = "**.**.**-***-**" return @@ -97,9 +97,7 @@ For more usage and examples see the [Godoc](http://godoc.org/github.com/ln80/str - `fullname` ## Limitations -1. Only fields of types convertible to `string` or `*string` are supported, although nesting structs directly or through collections (slices and maps) is also supported. +1. Only fields of types convertible to `string` or `*string` are supported, although nesting structs directly or through collections (slices and maps) is also supported. String slices (`[]string`, `[]*string`) are supported as data fields. 2. Self-Referencing Types are supported, allowing types to include fields of the same type. However, Self-Referencing Values (instances that create a reference loop) are not supported. -3. At the moment, collections of types convertible to `string` or `*string` are not supported. - diff --git a/doc.go b/doc.go index e015d41..8d66d71 100644 --- a/doc.go +++ b/doc.go @@ -24,7 +24,7 @@ Here's an example: Applying the default masking logic: - var profile := Profile{ + profile := Profile{ Email: "eric.prosacco@example.com", Fullname: "Eric Prosacco", Role: "Teacher", @@ -35,7 +35,7 @@ Applying the default masking logic: // After masking: // // Profile{ - // Email: "****.********@example.com", + // Email: "*************@example.com", // Fullname: "*************", // Role: "Teacher", // } @@ -54,7 +54,7 @@ Applying a custom redact logic: Role string } - var profile := Profile{ + profile := Profile{ Email: "eric.prosacco@example.com", Fullname: "Eric Prosacco", Role: "Teacher", diff --git a/mask.go b/mask.go index eedd9ca..8047d53 100644 --- a/mask.go +++ b/mask.go @@ -48,31 +48,38 @@ type Masked[T any] struct { } type MaskedCopyConfig struct { - DeepCopy bool // default false + // DeepCopy controls whether the original value is deep-copied before masking. + // Defaults to true. Setting to false is unsafe when the struct contains pointer, + // slice, or map fields, as Reveal() may return partially masked data. + DeepCopy bool } // NewMaskedCopy returns a new masked copy of the given value. // It fails if it can't copy the value or the mask config is invalid. func NewMaskedCopy[T any](v T, opts ...func(*MaskedCopyConfig)) (*Masked[T], error) { cfg := MaskedCopyConfig{ - DeepCopy: false, + DeepCopy: true, } option.Apply(&cfg, opts) - var copy = v + masked := v if cfg.DeepCopy { - c, err := copystructure.Copy(v) + // Deep-copy the original to prevent shared pointers between original and masked values. + // Without this, masking through shared *string/slice/map fields corrupts the original. + orig, err := copystructure.Copy(v) if err != nil { return nil, errors.Join(ErrFailedToMaskCopy, err) } - copy = c.(T) + if err := Mask(&masked); err != nil { + return nil, errors.Join(ErrFailedToMaskCopy, err) + } + return &Masked[T]{value: masked, original: orig.(T)}, nil } - if err := Mask(©); err != nil { + if err := Mask(&masked); err != nil { return nil, errors.Join(ErrFailedToMaskCopy, err) } - - return &Masked[T]{value: copy, original: v}, nil + return &Masked[T]{value: masked, original: v}, nil } // MaskedCopy returns a masked copy of the given value. diff --git a/mask/email.go b/mask/email.go index 57edfb2..eff5212 100644 --- a/mask/email.go +++ b/mask/email.go @@ -3,6 +3,7 @@ package mask import ( "errors" "strings" + "unicode/utf8" "github.com/ln80/struct-sensitive/internal/option" ) @@ -23,16 +24,18 @@ func Email(email string, opts ...func(*Config[EmailConfig])) (string, error) { return "", errors.New("invalid email format") } + // Use rune count for correct multi-byte character handling + localRunes := []rune(parts[0]) + localRuneCount := utf8.RuneCountInString(parts[0]) + var local string - if cfg.Kind.KeepFirstAndLastChar && len(parts[0]) > 2 { - // Keep first and last character, mask the rest - firstChar := string([]rune(parts[0])[0]) - lastChar := string([]rune(parts[0])[len([]rune(parts[0]))-1]) - middle := strings.Repeat(string([]rune{cfg.Symbol}), len([]rune(parts[0]))-2) + if cfg.Kind.KeepFirstAndLastChar && localRuneCount > 2 { + firstChar := string(localRunes[0]) + lastChar := string(localRunes[localRuneCount-1]) + middle := strings.Repeat(string([]rune{cfg.Symbol}), localRuneCount-2) local = firstChar + middle + lastChar } else { - // Mask the entire local part - local = strings.Repeat(string([]rune{cfg.Symbol}), len(parts[0])) + local = strings.Repeat(string([]rune{cfg.Symbol}), localRuneCount) } domain := parts[1] diff --git a/mask/fullname.go b/mask/fullname.go index c7e920d..416ee16 100644 --- a/mask/fullname.go +++ b/mask/fullname.go @@ -2,6 +2,7 @@ package mask import ( "strings" + "unicode/utf8" "github.com/ln80/struct-sensitive/internal/option" ) @@ -9,22 +10,22 @@ import ( type FullNameConfig struct { } -// FullName masks a full name in the format "J*** ***** ***** D**". +// FullName masks a full name by revealing only the first character of the first and last words. func FullName(name string, opts ...func(*Config[FullNameConfig])) (string, error) { cfg := DefaultConfig(FullNameConfig{}) option.Apply(&cfg, opts) - // Split name into words words := strings.Fields(name) - // Mask all words except the first and last var builder strings.Builder for i, word := range words { + // Use rune count for correct multi-byte character handling + runeCount := utf8.RuneCountInString(word) if i == 0 || i == len(words)-1 { builder.WriteRune([]rune(word)[0]) - builder.WriteString(strings.Repeat(string(cfg.Symbol), len(word)-1)) - } else { // Middle words - builder.WriteString(strings.Repeat(string(cfg.Symbol), len(word))) + builder.WriteString(strings.Repeat(string(cfg.Symbol), runeCount-1)) + } else { + builder.WriteString(strings.Repeat(string(cfg.Symbol), runeCount)) } if i != len(words)-1 { builder.WriteRune(' ') diff --git a/mask/fullname_test.go b/mask/fullname_test.go index 0f2e3b3..3c384b9 100644 --- a/mask/fullname_test.go +++ b/mask/fullname_test.go @@ -21,7 +21,7 @@ func TestFullName(t *testing.T) { }, { Value: "X Æ A-12 Musk", - Want: "X ** **** M***", + Want: "X * **** M***", OK: true, }, }) diff --git a/mask/mask.go b/mask/mask.go index 346d02c..767b01d 100644 --- a/mask/mask.go +++ b/mask/mask.go @@ -18,7 +18,7 @@ func DefaultConfig[T any](t T) Config[T] { } } -// Masker presents the function type that masks must satisfy. +// Masker is the function type that masks must satisfy. type Masker[T any] func(val string, opts ...func(*Config[T])) (string, error) // defaultMasker is a masker that doesn't support options @@ -44,8 +44,9 @@ func Register(kind string, m defaultMasker) { // Of returns the specific default masker of the given kind. func Of(kind string) (m defaultMasker, found bool) { - maskMu.Lock() - defer maskMu.Unlock() + // Read-only access: use RLock to allow concurrent mask lookups + maskMu.RLock() + defer maskMu.RUnlock() m, found = maskRegistry[kind] return diff --git a/mask_test.go b/mask_test.go index b3a4ed3..a4f49dd 100644 --- a/mask_test.go +++ b/mask_test.go @@ -113,12 +113,12 @@ func TestMaskedCopy(t *testing.T) { Fullname: "Guadalupe Kemmer DDS", } - copy, err := NewMaskedCopy(profile) + cp, err := NewMaskedCopy(profile) if err != nil { t.Fatal("expect err be nil, got", err) } - maskedCopy := copy.Value() + maskedCopy := cp.Value() if reflect.DeepEqual(profile, maskedCopy) { t.Fatalf("expect not be equals %v, %v", profile, maskedCopy) @@ -132,3 +132,43 @@ func TestMaskedCopy(t *testing.T) { t.Fatalf("expect be equals %v, %v", profile, maskedCopy) } } + +func TestMaskedCopy_PointerFields(t *testing.T) { + original := Profile{ + ID: "usr-1", + Email: "email@example.com", + Fullname: "Guadalupe Kemmer DDS", + Phone: ptr("519-491-6780"), + Devices: []Device{ + {IPAddr: "169.251.207.194"}, + }, + } + + cp, err := NewMaskedCopy(original) + if err != nil { + t.Fatal("expect err be nil, got", err) + } + + revealed := cp.Reveal() + + if revealed.Email != "email@example.com" { + t.Fatalf("Reveal().Email: want original, got %q", revealed.Email) + } + if revealed.Fullname != "Guadalupe Kemmer DDS" { + t.Fatalf("Reveal().Fullname: want original, got %q", revealed.Fullname) + } + if *revealed.Phone != "519-491-6780" { + t.Fatalf("Reveal().Phone: want original, got %q", *revealed.Phone) + } + if revealed.Devices[0].IPAddr != "169.251.207.194" { + t.Fatalf("Reveal().Devices[0].IPAddr: want original, got %q", revealed.Devices[0].IPAddr) + } + + masked := cp.Value() + if masked.Email == "email@example.com" { + t.Fatal("Value().Email should be masked") + } + if *masked.Phone == "519-491-6780" { + t.Fatal("Value().Phone should be masked") + } +} diff --git a/redact.go b/redact.go index 120b556..705753c 100644 --- a/redact.go +++ b/redact.go @@ -11,7 +11,7 @@ var ( ErrRedactFuncNotFound = errors.New("redact function not found") ) -// RedactConfig presents the configuration required by `sensitive.Redact`. +// RedactConfig holds the configuration for [Redact]. type RedactConfig struct { // RequireSubjectID force the subjectID resolution from the struct value. // This config is disabled by default. diff --git a/scan.go b/scan.go index f6ad0c5..7c4fd5a 100644 --- a/scan.go +++ b/scan.go @@ -78,9 +78,7 @@ func Scan(v any, requireSubject bool) (accessor Struct, err error) { err = fmt.Errorf("%w '%v'", ErrUnsupportedType, tt) return } - if tt.Kind() == reflect.Pointer { - tt = tt.Elem() - } + tt = tt.Elem() if tt.Kind() != reflect.Struct { err = fmt.Errorf("%w '%v'", ErrUnsupportedType, tt) return @@ -131,6 +129,7 @@ type sensitiveStructContext struct { type sensitiveField struct { sf reflect.StructField isSub, isData, isNested bool + isDataSlice bool prefix string isSlice, isMap bool nestedStructType *sensitiveStructType @@ -147,7 +146,6 @@ func (f sensitiveField) getType(cache map[reflect.Type]*sensitiveStructType) *se } func (f sensitiveField) IsZero() bool { - // TBD find a better condition?? return f.sf.Name == "" } @@ -188,10 +186,13 @@ func resolveSubject(pt sensitiveStructType, pv reflect.Value) (string, error) { continue } - cacheMu.Lock() + // Read-only cache access: use RLock to allow concurrent lookups + cacheMu.RLock() ssT := ssField.getType(cache) - cacheMu.Unlock() - // I believe ssT can't be nil + cacheMu.RUnlock() + if ssT == nil { + panic("struct-sensitive: cached nested struct type is unexpectedly nil") + } ssTv := *ssT sensitiveFieldV = reflect.Indirect(sensitiveFieldV) nestedSubject := "" @@ -268,6 +269,42 @@ func (s sensitiveStruct) Replace(fn ReplaceFunc) error { } elem := reflect.Indirect(v) + if ssField.isData && ssField.isDataSlice { + for i := 0; i < elem.Len(); i++ { + el := reflect.Indirect(elem.Index(i)) + var val string + if el.Type().ConvertibleTo(stringType) { + val = el.Convert(stringType).String() + } else { + val = el.String() + } + + newVal, err = fn(FieldReplace{ + SubjectID: s.subjectID, + Name: ssField.sf.Name, + RType: ssField.sf.Type, + Kind: ssField.kind, + Options: ssField.options, + }, val) + if err != nil { + return err + } + + if newVal != val { + switch el.Kind() { + case reflect.String: + el.SetString(newVal) + default: + vv := reflect.ValueOf(newVal) + if vv.IsValid() && vv.Type().ConvertibleTo(el.Type()) { + el.Set(vv.Convert(el.Type())) + } + } + } + } + continue + } + if ssField.isData { var val string if elem.Type().ConvertibleTo(stringType) { @@ -278,6 +315,7 @@ func (s sensitiveStruct) Replace(fn ReplaceFunc) error { newVal, err = fn(FieldReplace{ SubjectID: s.subjectID, + Name: ssField.sf.Name, RType: ssField.sf.Type, Kind: ssField.kind, Options: ssField.options, @@ -287,7 +325,15 @@ func (s sensitiveStruct) Replace(fn ReplaceFunc) error { } if newVal != val { - switch ssField.sf.Type.Kind() { + // Zero out original []byte backing array to prevent sensitive data + // from lingering in memory. Unlike string (immutable), []byte can be scrubbed. + if elem.Kind() == reflect.Slice && elem.Type().Elem().Kind() == reflect.Uint8 { + for i := range elem.Bytes() { + elem.Bytes()[i] = 0 + } + } + + switch elem.Kind() { case reflect.String: elem.SetString(newVal) default: @@ -303,11 +349,13 @@ func (s sensitiveStruct) Replace(fn ReplaceFunc) error { if ssField.isNested { var ssT sensitiveStructType - cacheMu.Lock() + // Read-only cache access: use RLock to allow concurrent lookups + cacheMu.RLock() ssTPtr := ssField.getType(cache) - cacheMu.Unlock() - - // I believe ssTPtr can't be nil + cacheMu.RUnlock() + if ssTPtr == nil { + panic("struct-sensitive: cached nested struct type is unexpectedly nil") + } ssT = *ssTPtr if !ssT.hasSensitive { continue @@ -331,7 +379,7 @@ func (s sensitiveStruct) Replace(fn ReplaceFunc) error { if mapElem.IsZero() { continue } - mapElem = reflect.Indirect(elem.MapIndex(k)) + mapElem = reflect.Indirect(mapElem) if !mapElem.CanAddr() { newElem := reflect.New(mapElem.Type()).Elem() newElem.Set(mapElem) @@ -372,23 +420,32 @@ func (s sensitiveStruct) Replace(fn ReplaceFunc) error { } func scanStructType(rt reflect.Type) (sensitiveStructType, error) { + // Fast path: check cache with read lock (avoids exclusive lock contention on cache hits) + cacheMu.RLock() + if cached, ok := cache[rt]; ok { + cacheMu.RUnlock() + return *cached, nil + } + cacheMu.RUnlock() + + // Slow path: acquire exclusive lock for cache population cacheMu.Lock() defer cacheMu.Unlock() - - if _, ok := cache[rt]; !ok { - c := sensitiveStructContext{seen: cache} - ssT, err := scanStructTypeWithContext(c, rt) - if err != nil { - return sensitiveStructType{}, err - } - cache[rt] = &ssT + if cached, ok := cache[rt]; ok { + return *cached, nil } + c := sensitiveStructContext{seen: cache} + ssT, err := scanStructTypeWithContext(c, rt) + if err != nil { + return sensitiveStructType{}, err + } + cache[rt] = &ssT return *cache[rt], nil } func scanStructTypeWithContext(c sensitiveStructContext, rt reflect.Type) (sensitiveStructType, error) { - sensitiveFields := make([]sensitiveField, 0) + sensitiveFields := make([]sensitiveField, 0, rt.NumField()) var subjectField sensitiveField for i := 0; i < rt.NumField(); i++ { field := rt.Field(i) @@ -427,7 +484,21 @@ func scanStructTypeWithContext(c sensitiveStructContext, rt reflect.Type) (sensi if tt.Kind() == reflect.Ptr { tt = tt.Elem() } - if tt.Kind() != reflect.String && !field.Type.ConvertibleTo(stringType) { + // Support []T where T has Kind string (e.g., []string, []*string). + // Exclude []byte: the whole slice is convertible to string and handled below. + if tt.Kind() == reflect.Slice && !tt.ConvertibleTo(stringType) { + elemType := tt.Elem() + if elemType.Kind() == reflect.Ptr { + elemType = elemType.Elem() + } + if elemType.Kind() == reflect.String { + ssField.isDataSlice = true + sensitiveFields = append(sensitiveFields, ssField) + } + continue + } + // Use tt (pointer-unwrapped type) for the convertibility check, not field.Type + if tt.Kind() != reflect.String && !tt.ConvertibleTo(stringType) { continue } sensitiveFields = append(sensitiveFields, ssField) diff --git a/scan_test.go b/scan_test.go index de2c6a3..3b8a6c6 100644 --- a/scan_test.go +++ b/scan_test.go @@ -439,6 +439,57 @@ func TestScan(t *testing.T) { ok: true, } }(), + func() tc { + type T struct { + ID string `sensitive:"subjectID"` + Tags []string `sensitive:"data"` + } + return tc{ + val: &T{ + ID: "abc", + Tags: []string{"tag1", "tag2", "tag3"}, + }, + want: &T{ + ID: "abc", + Tags: []string{"", "", ""}, + }, + ok: true, + } + }(), + func() tc { + type T struct { + ID string `sensitive:"subjectID"` + Tags []string `sensitive:"data"` + } + return tc{ + val: &T{ + ID: "abc", + Tags: nil, + }, + want: &T{ + ID: "abc", + Tags: nil, + }, + ok: true, + } + }(), + func() tc { + type T struct { + ID string `sensitive:"subjectID"` + Names []*string `sensitive:"data"` + } + return tc{ + val: &T{ + ID: "abc", + Names: []*string{ptr("Alice"), ptr("Bob")}, + }, + want: &T{ + ID: "abc", + Names: []*string{ptr(""), ptr("")}, + }, + ok: true, + } + }(), } // replaceFn does empty sensitive fields. This particular behavior makes testing easier. diff --git a/tag.go b/tag.go index d4709e5..1a43b55 100644 --- a/tag.go +++ b/tag.go @@ -14,7 +14,7 @@ var ( tagDive = "dive" ) -// TagOptions presents a map of options configured at the `sensitive` tag. +// TagOptions is a map of options configured in the `sensitive` tag. type TagOptions map[string]string func (m TagOptions) Get(name string) string { From 5c3c7a1ee8bf7f090ba53754dd55990edc605cc5 Mon Sep 17 00:00:00 2001 From: Reda Laanait Date: Thu, 28 May 2026 10:55:26 +0100 Subject: [PATCH 3/5] chore: bump go version --- .github/workflows/module.yml | 2 +- go.mod | 9 +++++---- go.sum | 5 +++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/module.yml b/.github/workflows/module.yml index b87e4de..2a0d4bc 100644 --- a/.github/workflows/module.yml +++ b/.github/workflows/module.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [ "1.22", "1.23" ] + go-version: ["1.24", "1.25", "1.26" ] steps: - name: Setup Go diff --git a/go.mod b/go.mod index 0bfba04..55edda0 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module github.com/ln80/struct-sensitive -go 1.22.0 +go 1.24.0 require ( - github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/sanity-io/litter v1.5.5 // indirect + github.com/mitchellh/copystructure v1.2.0 + github.com/sanity-io/litter v1.5.5 ) + +require github.com/mitchellh/reflectwalk v1.0.2 // indirect diff --git a/go.sum b/go.sum index 91b9d88..fdb0d62 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,14 @@ +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b h1:XxMZvQZtTXpWMNWK82vdjCLCe7uGMFXdTsJH0v3Hkvw= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0 h1:GD+A8+e+wFkqje55/2fOVnZPkoDIu1VooBWfNrnY8Uo= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= +github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg= +github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312 h1:UsFdQ3ZmlzS0BqZYGxvYaXvFGUbCmPGy8DM7qWJJiIQ= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= From 6d048027cffec65d9394c1e2e8a60e59670cc603 Mon Sep 17 00:00:00 2001 From: Reda Laanait Date: Thu, 28 May 2026 10:57:20 +0100 Subject: [PATCH 4/5] chore: bump go version --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 282f8d1..af58f99 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,7 +15,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.22 + go-version: 1.24 - name: Check out code uses: actions/checkout@v4 From 806b6d0f039a3801f98bcc7b8db3c1903e280a1f Mon Sep 17 00:00:00 2001 From: Reda Laanait Date: Thu, 28 May 2026 11:01:02 +0100 Subject: [PATCH 5/5] chore: bump go version --- .github/workflows/lint.yml | 6 +++--- .github/workflows/module.yml | 2 +- .github/workflows/security.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index af58f99..ee230eb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: 1.24 @@ -21,7 +21,7 @@ jobs: uses: actions/checkout@v4 - name: Lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v9 with: - version: v1.60 + version: v2.12 args: --enable misspell \ No newline at end of file diff --git a/.github/workflows/module.yml b/.github/workflows/module.yml index 2a0d4bc..2f0bec5 100644 --- a/.github/workflows/module.yml +++ b/.github/workflows/module.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 149b4a4..c686b27 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -12,9 +12,9 @@ jobs: steps: - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: - go-version: 1.22 + go-version: 1.24 - name: Check out code uses: actions/checkout@v4