From 04bcfc03fbef2b877116dc35c495bec58a83be30 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Fri, 27 Mar 2026 09:58:14 +0300 Subject: [PATCH 1/2] fix(ci): add .exe suffix for Windows hx build The Windows hx-binaries job was failing because `go build -o .bin/hx` doesn't auto-append .exe when -o is explicit, but the workflow rename step expected hx.exe. This caused hx-upload-release to be skipped, leaving releases with no binary assets. --- cmd/hx/Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/hx/Makefile b/cmd/hx/Makefile index 3b618cd..2f67bd0 100644 --- a/cmd/hx/Makefile +++ b/cmd/hx/Makefile @@ -2,9 +2,10 @@ VERSION ?= dev LDFLAGS := -s -w -X main.version=$(VERSION) +EXT := $(if $(filter windows,$(GOOS)),.exe,) build: - CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o .bin/hx . + CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o .bin/hx$(EXT) . install: go install . From 0c9e84f48c93cb0a7e590583298d65ab09556108 Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Tue, 31 Mar 2026 17:13:42 +0300 Subject: [PATCH 2/2] chore: make MatchItem case insensitive --- collections/slice.go | 21 ++++++++++++++------- collections/slice_test.go | 6 ++++++ duration/duration.go | 21 +++++++++++++++++---- duration/manual_test.go | 29 +++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 duration/manual_test.go diff --git a/collections/slice.go b/collections/slice.go index afc4352..d1797ef 100644 --- a/collections/slice.go +++ b/collections/slice.go @@ -209,24 +209,31 @@ func MatchItems(item string, patterns ...string) bool { } func matchPattern(item, pattern string) bool { - if pattern == "*" || item == pattern { + if pattern == "*" { return true } - if strings.HasPrefix(pattern, "*") && strings.HasSuffix(pattern, "*") { - if strings.Contains(item, strings.TrimPrefix(strings.TrimSuffix(pattern, "*"), "*")) { + itemLower := strings.ToLower(item) + patternLower := strings.ToLower(pattern) + + if itemLower == patternLower { + return true + } + + if strings.HasPrefix(patternLower, "*") && strings.HasSuffix(patternLower, "*") { + if strings.Contains(itemLower, strings.TrimPrefix(strings.TrimSuffix(patternLower, "*"), "*")) { return true } } - if strings.HasPrefix(pattern, "*") { - if strings.HasSuffix(item, strings.TrimPrefix(pattern, "*")) { + if strings.HasPrefix(patternLower, "*") { + if strings.HasSuffix(itemLower, strings.TrimPrefix(patternLower, "*")) { return true } } - if strings.HasSuffix(pattern, "*") { - if strings.HasPrefix(item, strings.TrimSuffix(pattern, "*")) { + if strings.HasSuffix(patternLower, "*") { + if strings.HasPrefix(itemLower, strings.TrimSuffix(patternLower, "*")) { return true } } diff --git a/collections/slice_test.go b/collections/slice_test.go index 7bbd6f3..a8484c5 100644 --- a/collections/slice_test.go +++ b/collections/slice_test.go @@ -35,6 +35,12 @@ var _ = Describe("MatchItems", func() { Entry("URL encoded pattern matches", "hello ", []string{"hello%20"}, true), Entry("URL encoded pattern does not match", "hello", []string{"hello%20"}, false), Entry("malformed URL encoding", "apple", []string{"%zzapple"}, false), + Entry("case insensitive exact match", "Apple", []string{"apple"}, true), + Entry("case insensitive exact match reverse", "apple", []string{"Apple"}, true), + Entry("case insensitive prefix wildcard", "Apple", []string{"appl*"}, true), + Entry("case insensitive suffix wildcard", "Apple", []string{"*PLE"}, true), + Entry("case insensitive glob", "Apple", []string{"*PPL*"}, true), + Entry("case insensitive exclusion", "Apple", []string{"!apple"}, false), ) }) diff --git a/duration/duration.go b/duration/duration.go index c3b51a8..a65135f 100644 --- a/duration/duration.go +++ b/duration/duration.go @@ -83,10 +83,8 @@ func (d Duration) String() string { d = d - d%Duration(time.Minute) } else if d.Minutes() > 2 { d = d - d%Duration(time.Second) - } else if d.Seconds() > 2 { - d = d - d%Duration(time.Millisecond) } else if d.Nanoseconds() > 2*1000*1000 { - d = d - d%Duration(time.Microsecond) + d = d - d%Duration(time.Millisecond) } // Largest time is 2540400h10m10.000000000s var buf [32]byte @@ -213,7 +211,22 @@ func (d Duration) String() string { buf[w] = '-' } - return strings.ReplaceAll(strings.ReplaceAll(string(buf[w:]), "0s", ""), "0m", "") + result := string(buf[w:]) + // Strip standalone zero-valued units "0m" (minutes) and "0s" (seconds). + // Must not match inside sub-second units like "320ms", "500µs", "100ns". + + // Strip "0m" only if NOT followed by "s" (which would make it "0ms" = milliseconds) + if i := strings.Index(result, "0m"); i >= 0 { + if i+2 >= len(result) || result[i+2] != 's' { + result = result[:i] + result[i+2:] + } + } + // Strip "0s" only at the very end, and only if the result contains + // larger units (meaning "0s" is a trailing zero seconds, not "0s" standalone) + if strings.HasSuffix(result, "0s") && len(result) > 2 { + result = result[:len(result)-2] + } + return result } // fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the diff --git a/duration/manual_test.go b/duration/manual_test.go new file mode 100644 index 0000000..cac2f42 --- /dev/null +++ b/duration/manual_test.go @@ -0,0 +1,29 @@ +package duration + +import ( + "fmt" + "testing" + "time" +) + +func TestMillisecondFormatting(t *testing.T) { + tests := []struct { + d time.Duration + expected string + }{ + {320 * time.Millisecond, "320ms"}, + {150 * time.Millisecond, "150ms"}, + {5250 * time.Millisecond, "5.25s"}, + {1250 * time.Millisecond, "1.25s"}, + {999 * time.Millisecond, "999ms"}, + {1001 * time.Millisecond, "1.001s"}, + {50 * time.Millisecond, "50ms"}, + } + for _, tc := range tests { + got := Duration(tc.d).String() + if got != tc.expected { + t.Errorf("%v: got %q, want %q", tc.d, got, tc.expected) + } + fmt.Printf("%v -> %s (want %s)\n", tc.d, got, tc.expected) + } +}