diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da5ce31..744219c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,9 +24,9 @@ jobs: with: go-version-file: go.mod cache: true - - uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9 + - uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 with: - version: v2.11.4 + version: v2.13.1 tidy: name: Go mod tidy diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml index 8127719..ee8429e 100644 --- a/.github/workflows/gitleaks.yml +++ b/.github/workflows/gitleaks.yml @@ -23,7 +23,7 @@ jobs: with: # gitleaks scans the full history by default; fetch all commits. fetch-depth: 0 - - uses: gitleaks/gitleaks-action@dcedce43c6f43de0b836d1fe38946645c9c638dc # v2.3.9 + - uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0 env: # github.token is required; gitleaks uses it to post scan summaries. GITHUB_TOKEN: ${{ github.token }} diff --git a/CHANGELOG.md b/CHANGELOG.md index a302bd6..0e4c329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,55 @@ All notable changes to **go-github-kit** are documented in this file. The format is based on [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.9.0] - 2026-09-01 + +Maintenance release. Moves the module to Go 1.27, applies `go fix ./...`, +and refreshes the pinned GitHub Actions. No exported signature, type shape, +sentinel or message string changed, and no library behaviour changed. + +This raises the consumer floor: the `go` directive moves from `1.26` to +`1.27`, so a consumer building on Go 1.26 must upgrade. That is the only +consumer-visible effect of this release. A `toolchain` directive in a +dependency is still ignored, so the `go1.27.0` pin imposes nothing +downstream. + +### Changed + +- `go.mod` and `examples/go.mod`: `go 1.26` to `go 1.27`, and + `toolchain go1.26.6` to `toolchain go1.27.0`. The directive is a minor + floor, not a patch-level one, so `GOTOOLCHAIN=local` builds keep working + on any 1.27 patch. +- `retry`: the four `errors.As` calls in `IsTransientNetErr` and + `isPermanentNetErr` that only tested for a match, and never read the + bound variable, are now `errors.AsType`. This is the rewrite `go fix` + applies; `errors.AsType` has been available since Go 1.26, so the floor + rise above is not caused by it. The predicates classify exactly the same + errors as before. The two remaining `errors.As` calls are unchanged, + because both use the value they bind. +- `etag/algo_test.go` reformatted. Go 1.27's `gofmt` changed how it groups + alignment runs in map literals, and this file is the only one in the + tree it repositions. Test data and assertions are untouched. +- Test-only: `atomic.AddInt32`/`atomic.LoadInt32` over an `int32` replaced + with `atomic.Int32` methods in the `etag`, `pages` and `ratelimit` tests, + again from `go fix`. + +### CI + +- `golangci-lint` pinned version bumped from v2.11.4 to v2.13.1. This is + required by the toolchain bump rather than routine: v2.11.4 predates + Go 1.27 and carries a Go 1.26 formatter, so it would reject the + `etag/algo_test.go` reformat above and fail the lint job. +- `gitleaks/gitleaks-action` bumped from v2.3.9 to v3.0.0. v3 is a runtime + migration from Node 20 to Node 24 with no change to inputs, outputs or + behaviour. GitHub removes Node 20 from hosted runners on 2026-09-16, + after which v2 stops running at all. +- `actions/checkout` (v7.0.1), `actions/setup-go` (v7.0.0), + `actions/upload-artifact` (v7.0.1), `codecov/codecov-action` (v7.0.0) and + `golangci/golangci-lint-action` (v9.3.0) were each checked against their + latest release and are already pinned to it; no SHA changed. The + `golangci-lint-action` pin comment now names the exact version, v9.3.0, + matching every other pin in the tree. + ## [1.8.0] - 2026-08-20 Ergonomics release for go-github v87 and later. Adds `ghkit.Adapt`, which @@ -858,6 +907,7 @@ and rotating PATs alike. - `golang.org/x/oauth2` v0.36.0 - `golang.org/x/time` v0.15.0 +[1.9.0]: https://github.com/pcanilho/go-github-kit/releases/tag/v1.9.0 [1.8.0]: https://github.com/pcanilho/go-github-kit/releases/tag/v1.8.0 [1.7.0]: https://github.com/pcanilho/go-github-kit/releases/tag/v1.7.0 [1.6.2]: https://github.com/pcanilho/go-github-kit/releases/tag/v1.6.2 diff --git a/etag/algo_test.go b/etag/algo_test.go index 1819121..e8ddc1a 100644 --- a/etag/algo_test.go +++ b/etag/algo_test.go @@ -156,12 +156,12 @@ func TestETag_NormalisePath(t *testing.T) { "/repos/google/go-github": "/repos/{o}/{r}", "/repos/google/go-github/commits/abc1234567": "/repos/{o}/{r}/commits/{sha}", "/repos/google/go-github/compare/main...feature": "/repos/{o}/{r}/compare/{base...head}", - testPathOctocat: "/users/{u}", - "/orgs/github": "/orgs/{o}", - "/app/installations/12345": "/app/installations/{id}", - "/meta": "/meta", - "/gists/1234": "/gists/_", // unknown-route fallback - "/unmapped": "unknown", + testPathOctocat: "/users/{u}", + "/orgs/github": "/orgs/{o}", + "/app/installations/12345": "/app/installations/{id}", + "/meta": "/meta", + "/gists/1234": "/gists/_", // unknown-route fallback + "/unmapped": "unknown", } for in, want := range cases { if got := normalisePath(in); got != want { diff --git a/etag/transport_test.go b/etag/transport_test.go index b4c2bb1..b99dc8c 100644 --- a/etag/transport_test.go +++ b/etag/transport_test.go @@ -367,11 +367,11 @@ func TestETag_LinkHeaderPreservedOn304Replay(t *testing.T) { func TestETag_WriteInvalidation_404(t *testing.T) { body := []byte("x") - var count int32 + var count atomic.Int32 var lastIfNoneMatch atomic.Value // string lastIfNoneMatch.Store("") s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - n := atomic.AddInt32(&count, 1) + n := count.Add(1) lastIfNoneMatch.Store(r.Header.Get("If-None-Match")) switch n { case 1: @@ -418,7 +418,7 @@ func TestETag_WriteInvalidation_404(t *testing.T) { if err := r3.Body.Close(); err != nil { t.Fatalf("post-invalidation body close: %v", err) } - if got := atomic.LoadInt32(&count); got != 3 { + if got := count.Load(); got != 3 { t.Fatalf("expected 3 upstream requests after invalidation; got %d", got) } if got := lastIfNoneMatch.Load().(string); got != "" { diff --git a/examples/go.mod b/examples/go.mod index c9ea6e7..aad687b 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -1,8 +1,8 @@ module github.com/pcanilho/go-github-kit/examples -go 1.26 +go 1.27 -toolchain go1.26.6 +toolchain go1.27.0 require ( github.com/bradleyfalzon/ghinstallation/v2 v2.19.0 diff --git a/go.mod b/go.mod index b958087..a3cbb84 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/pcanilho/go-github-kit -go 1.26 +go 1.27 -toolchain go1.26.6 +toolchain go1.27.0 require ( github.com/gofri/go-github-ratelimit/v2 v2.0.2 diff --git a/pages/bench_test.go b/pages/bench_test.go index 72ff398..101d68d 100644 --- a/pages/bench_test.go +++ b/pages/bench_test.go @@ -123,10 +123,10 @@ func BenchmarkPages_AsTyped(b *testing.B) { // changes to caching behaviour show up here, not just in the latency // numbers above. func BenchmarkPages_HandlerHits(b *testing.B) { - var hits int32 + var hits atomic.Int32 var srv *httptest.Server srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddInt32(&hits, 1) + hits.Add(1) page := 1 if p := r.URL.Query().Get("page"); p != "" { page, _ = strconv.Atoi(p) @@ -145,5 +145,5 @@ func BenchmarkPages_HandlerHits(b *testing.B) { for range b.N { drainPages(b, hc, srv.URL+"/items") } - b.ReportMetric(float64(atomic.LoadInt32(&hits))/float64(b.N), "hits/walk") + b.ReportMetric(float64(hits.Load())/float64(b.N), "hits/walk") } diff --git a/pages/pages_test.go b/pages/pages_test.go index aff5082..6d775a0 100644 --- a/pages/pages_test.go +++ b/pages/pages_test.go @@ -218,11 +218,11 @@ func TestPages_304PreservesLink(t *testing.T) { // second range over the same URL hits the 304 path on every page; // the etag layer's merged-headers code must preserve the Link // header so the iterator still walks page 2. - var hits int32 + var hits atomic.Int32 var srv *httptest.Server mux := http.NewServeMux() mux.HandleFunc("/items", func(w http.ResponseWriter, r *http.Request) { - atomic.AddInt32(&hits, 1) + hits.Add(1) page := 1 if p := r.URL.Query().Get("page"); p != "" { page, _ = strconv.Atoi(p) @@ -261,7 +261,7 @@ func TestPages_304PreservesLink(t *testing.T) { if got := walk(); got != 2 { t.Fatalf("first walk pageCount = %d, want 2", got) } - firstHits := atomic.LoadInt32(&hits) + firstHits := hits.Load() // Second walk: each page should round-trip but resolve to 304. The // Link header survives the 304 path, so the iterator still walks 2 @@ -269,7 +269,7 @@ func TestPages_304PreservesLink(t *testing.T) { if got := walk(); got != 2 { t.Fatalf("second walk pageCount = %d, want 2", got) } - if got := atomic.LoadInt32(&hits); got != firstHits*2 { + if got := hits.Load(); got != firstHits*2 { t.Errorf("hits after second walk = %d, want %d (one round-trip per page)", got, firstHits*2) } } @@ -346,11 +346,11 @@ func TestAs_DecodeError(t *testing.T) { // closeCounter wraps a body so the test can assert close behaviour. type closeCounter struct { io.Reader - closes int32 + closes atomic.Int32 } func (c *closeCounter) Close() error { - atomic.AddInt32(&c.closes, 1) + c.closes.Add(1) return nil } @@ -381,7 +381,7 @@ func TestAs_BodyClosedAfterDecode(t *testing.T) { if elems != 2 { t.Errorf("elems = %d, want 2", elems) } - if got := atomic.LoadInt32(&cc.closes); got != 1 { + if got := cc.closes.Load(); got != 1 { t.Errorf("body closes = %d, want 1", got) } } @@ -407,7 +407,7 @@ func TestAs_BodyClosedOnDecodePanic(t *testing.T) { // Recover the panic; we only care that the body got closed // before unwinding past the iterator. _ = recover() - if got := atomic.LoadInt32(&cc.closes); got != 1 { + if got := cc.closes.Load(); got != 1 { t.Errorf("body closes after panic = %d, want 1", got) } }() @@ -449,7 +449,7 @@ func TestAs_BodyClosedOnCallerBreak(t *testing.T) { } // The body for the page we touched must be closed even though we // broke before consuming all elements. - if got := atomic.LoadInt32(&cc.closes); got != 1 { + if got := cc.closes.Load(); got != 1 { t.Errorf("body closes = %d, want 1", got) } } diff --git a/ratelimit/ratelimit_test.go b/ratelimit/ratelimit_test.go index 655999b..8f3ef8e 100644 --- a/ratelimit/ratelimit_test.go +++ b/ratelimit/ratelimit_test.go @@ -37,9 +37,9 @@ func (s *syncBuf) String() string { // observe the primary-limit callback firing. func rlPrimaryServer(t *testing.T, limitResponses int) *httptest.Server { t.Helper() - var seen int32 + var seen atomic.Int32 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if int(atomic.AddInt32(&seen, 1)) <= limitResponses { + if int(seen.Add(1)) <= limitResponses { w.Header().Set("X-RateLimit-Remaining", "0") w.Header().Set("X-RateLimit-Reset", "1") w.Header().Set("X-RateLimit-Resource", "core") diff --git a/retry/retry.go b/retry/retry.go index adb4480..782b3e1 100644 --- a/retry/retry.go +++ b/retry/retry.go @@ -132,12 +132,10 @@ func IsTransientNetErr(err error) bool { if isPermanentNetErr(err) { return false } - var opErr *net.OpError - if errors.As(err, &opErr) { + if _, ok := errors.AsType[*net.OpError](err); ok { return true } - var dnsErr *net.DNSError - if errors.As(err, &dnsErr) { + if _, ok := errors.AsType[*net.DNSError](err); ok { // IsNotFound was filtered out above; remaining DNS errors (server // failure, timeout) are transient. return true @@ -162,12 +160,10 @@ func isPermanentNetErr(err error) bool { if errors.Is(err, syscall.ECONNREFUSED) { return true } - var unknownAuth x509.UnknownAuthorityError - if errors.As(err, &unknownAuth) { + if _, ok := errors.AsType[x509.UnknownAuthorityError](err); ok { return true } - var hostnameErr *x509.HostnameError - if errors.As(err, &hostnameErr) { + if _, ok := errors.AsType[*x509.HostnameError](err); ok { return true } var certInvalid x509.CertificateInvalidError