From 278d34f746a9f052b67bf237e70876af4a1753fe Mon Sep 17 00:00:00 2001 From: Constantine Nathanson Date: Tue, 25 Aug 2026 17:31:32 +0300 Subject: [PATCH 1/4] Update supported Go versions and modernize deprecated APIs Shift the CI matrix and README support table to Go 1.24-1.26, and clean up code that no longer passes vet/lint on current toolchains: - Replace deprecated `ioutil.ReadAll`/`TempFile` with `io.ReadAll`/`os.CreateTemp` - Replace deprecated `strings.Title` with an explicit first-rune upper - Replace deprecated `rand.Seed` with a local `rand.New` source - Use a typed context key for `api_version` instead of a bare string, fixing the `SA1029` staticcheck violation - Use a raw string literal for the base64 detection regex - Drop the duplicated `omitempty` on `UpdateAssetParams.Tags` - Remove unused test constants and unused assignments Also fix two latent test/code bugs surfaced while cleaning up: - `Upload` discarded the error from `HandleRawResponse` and returned nil - `TestUploader_UploadWithResponsiveBreakpoints` dereferenced a nil response after `t.Error`; use `t.Fatal` - Register `getAutoVideoDetailsTestCases` in the acceptance suite, which was defined but never run Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yaml | 6 ++---- README.md | 10 +++++----- api/admin/analysis.go | 6 +++++- api/admin/api.go | 5 ++--- api/admin/asset.go | 2 +- api/api.go | 2 +- api/uploader/upload.go | 3 +-- api/uploader/upload_acceptance_test.go | 3 +++ api/uploader/upload_asset.go | 2 +- api/uploader/upload_asset_test.go | 5 ++--- asset/auth_token_test.go | 3 --- cloudinary_test.go | 2 +- config/configuration_test.go | 2 +- gen/generate_setters/template.go | 3 ++- internal/cldtest/cldtest.go | 7 ++----- scripts/allocate_test_cloud.go | 4 ++-- 16 files changed, 31 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e7f743b..11605fe 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,11 +11,9 @@ jobs: fail-fast: false matrix: go-version: - - 1.20.x - - 1.21.x - - 1.22.x - - 1.23.x - 1.24.x + - 1.25.x + - 1.26.x steps: - name: ๐Ÿ”„ Checkout Repository diff --git a/README.md b/README.md index f3904dd..380fe01 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,11 @@ For the complete documentation, see the [Go SDK Guide](https://cloudinary.com/do ## Version Support -| **SDK Version** | **Go 1.13 - 1.19** | **Go 1.20** | **Go 1.21** | **Go 1.22** | **Go 1.23** | -|-----------------|--------------------|-------------|-------------|-------------|-------------| -| **2.8 & Up** | โŒ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | -| **2.7** | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | -| **1.x** | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | +| **SDK Version** | **Go 1.13 - 1.19** | **Go 1.20 - 1.23** | **Go 1.24** | **Go 1.25** | **Go 1.26** | +|-----------------|--------------------|--------------------|-------------|-------------|-------------| +| **2.8 & Up** | โŒ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | +| **2.7** | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | +| **1.x** | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | ## Installation diff --git a/api/admin/analysis.go b/api/admin/analysis.go index 416b567..84b66a1 100644 --- a/api/admin/analysis.go +++ b/api/admin/analysis.go @@ -5,6 +5,10 @@ import ( "github.com/cloudinary/cloudinary-go/v2/api" ) +type contextKey string + +const apiVersionKey contextKey = "api_version" + const ( analysis api.EndPoint = "analysis" analyze api.EndPoint = "analyze" @@ -46,7 +50,7 @@ Currently supports the following analysis options: * Custom */ func (a *API) Analyze(ctx context.Context, params AnalyzeParams) (*AnalyzeResult, error) { - v2APICtx := context.WithValue(ctx, "api_version", "2") + v2APICtx := context.WithValue(ctx, apiVersionKey, "2") res := &AnalyzeResult{} _, err := a.post(v2APICtx, api.BuildPath(analysis, analyze, uri), params, res) diff --git a/api/admin/api.go b/api/admin/api.go index 82c278d..9a7e08e 100644 --- a/api/admin/api.go +++ b/api/admin/api.go @@ -9,7 +9,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "mime/multipart" "net/http" "os" @@ -168,7 +167,7 @@ func (a *API) postFile(ctx context.Context, path interface{}, file interface{}, func (a *API) executeRequest(ctx context.Context, method string, path interface{}, body io.Reader, queryParams string, headers map[string]string, result interface{}) (*http.Response, error) { apiVersion := "" - if apiVersionRaw := ctx.Value("api_version"); apiVersionRaw != nil { + if apiVersionRaw := ctx.Value(apiVersionKey); apiVersionRaw != nil { apiVersion = apiVersionRaw.(string) } @@ -205,7 +204,7 @@ func (a *API) executeRequest(ctx context.Context, method string, path interface{ defer api.DeferredClose(resp.Body) - bodyBytes, err := ioutil.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, err } diff --git a/api/admin/asset.go b/api/admin/asset.go index 615c5a7..6137399 100644 --- a/api/admin/asset.go +++ b/api/admin/asset.go @@ -221,7 +221,7 @@ type UpdateAssetParams struct { BackgroundRemoval string `json:"background_removal,omitempty"` QualityOverride int `json:"quality_override,omitempty"` NotificationURL string `json:"notification_url,omitempty"` - Tags api.CldAPIArray `json:"tags,omitempty,omitempty"` + Tags api.CldAPIArray `json:"tags,omitempty"` Context api.CldAPIMap `json:"context,omitempty"` AdminContext []api.AdminContext `json:"admin_context,omitempty"` FaceCoordinates api.Coordinates `json:"face_coordinates,omitempty"` diff --git a/api/api.go b/api/api.go index 0c7843e..e1dc2b0 100644 --- a/api/api.go +++ b/api/api.go @@ -63,7 +63,7 @@ func BaseURL(uploadPrefix string, apiVer string) string { } // base64DataRegex is the regular expression for detecting base64 encoded strings. -var base64DataRegex = regexp.MustCompile("^data:([\\w-]+/[\\w\\-+.]+)?(;[\\w-]+=[\\w-]+)*;base64,([a-zA-Z0-9/+\\n=]+)$") +var base64DataRegex = regexp.MustCompile(`^data:([\w-]+/[\w\-+.]+)?(;[\w-]+=[\w-]+)*;base64,([a-zA-Z0-9/+\n=]+)$`) // AssetType is the type of the asset. type AssetType string diff --git a/api/uploader/upload.go b/api/uploader/upload.go index 835bbc9..f4c084a 100644 --- a/api/uploader/upload.go +++ b/api/uploader/upload.go @@ -12,7 +12,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "log" "mime/multipart" "net/http" @@ -358,7 +357,7 @@ func (u *API) postBody(ctx context.Context, urlPath interface{}, bodyReader io.R defer api.DeferredClose(resp.Body) - return ioutil.ReadAll(resp.Body) + return io.ReadAll(resp.Body) } func setAuth(u *API, req *http.Request) { diff --git a/api/uploader/upload_acceptance_test.go b/api/uploader/upload_acceptance_test.go index 38a6de2..1305c65 100644 --- a/api/uploader/upload_acceptance_test.go +++ b/api/uploader/upload_acceptance_test.go @@ -190,6 +190,8 @@ func getAutoVideoDetailsTestCases() []UploadAPIAcceptanceTestCase { AutoVideoDetails: &api.AutoVideoDetails{ Fields: []string{"title", "description"}, }, + Unsigned: api.Bool(true), + Timestamp: 123456789, }) }, ResponseTest: func(response interface{}, t *testing.T) {}, @@ -292,6 +294,7 @@ func TestUploadAPI_Acceptance(t *testing.T) { testUploadAPIByTestCases(getAuthorizationTestCases(), t) testUploadAPIByTestCases(getFolderDecouplingTestCases(), t) testUploadAPIByTestCases(getAutoTranscriptionTestCases(), t) + testUploadAPIByTestCases(getAutoVideoDetailsTestCases(), t) testUploadAPIByTestCases(getBooleanValuesTestCases(), t) testUploadAPIByTestCases(getVariousValuesTestCases(), t) testUploadAPIByTestCases(getUploadConfigTestCases(), t) diff --git a/api/uploader/upload_asset.go b/api/uploader/upload_asset.go index 18e138a..5d57cbf 100644 --- a/api/uploader/upload_asset.go +++ b/api/uploader/upload_asset.go @@ -128,7 +128,7 @@ func (u *API) Upload(ctx context.Context, file interface{}, uploadParams UploadP } err = api.HandleRawResponse(body, result) - return result, nil + return result, err } // Eager contains information about eagerly transformed derived assets. diff --git a/api/uploader/upload_asset_test.go b/api/uploader/upload_asset_test.go index 772d32d..7967970 100644 --- a/api/uploader/upload_asset_test.go +++ b/api/uploader/upload_asset_test.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "io/ioutil" "log" "os" "runtime" @@ -310,7 +309,7 @@ func TestUploader_UploadWithResponsiveBreakpoints(t *testing.T) { } if eResp == nil { - t.Error(resp) + t.Fatal(resp) } assert.Len(t, eResp.ResponsiveBreakpoints, 2) @@ -364,7 +363,7 @@ func populateLargeImage() string { "\xFC\x00\x00\x00\x00\x00\x00\x00\x00fff\xFC\x00\x00\x00\x00\x00\x00\x00\x00\xC4\xF5(\xFF\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - tmpFile, err := ioutil.TempFile(cldtest.TestDataDir(), largeImagePublicID+".*.bmp") + tmpFile, err := os.CreateTemp(cldtest.TestDataDir(), largeImagePublicID+".*.bmp") if err != nil { log.Fatal(err) } diff --git a/asset/auth_token_test.go b/asset/auth_token_test.go index 6edf6ed..7017b2b 100644 --- a/asset/auth_token_test.go +++ b/asset/auth_token_test.go @@ -8,14 +8,11 @@ import ( ) const authTokenKey = "00112233FF99" -const authTokenAltKey = "CCBB2233FF00" const duration = 300 const startTime = 11111111 const authTokenTestImage = "sample.jpg" -const authTokenTestConfigACL = "/*/t_foobar" -const authTokenTestPath = "http://res.cloudinary.com/test123/image/upload/v1486020273/sample.jpg" var authTokenConfig = config.AuthToken{ Duration: duration, diff --git a/cloudinary_test.go b/cloudinary_test.go index 86111e6..3887bd8 100644 --- a/cloudinary_test.go +++ b/cloudinary_test.go @@ -26,7 +26,7 @@ func TestCloudinary_CreateInstance(t *testing.T) { t.Error("Failed creating Cloudinary instance from Cloudinary URL.") } - c, err := NewFromURL("") + _, err := NewFromURL("") if err == nil || err.Error() != "must provide CLOUDINARY_URL" { t.Error("Error expected, got: ", err) } diff --git a/config/configuration_test.go b/config/configuration_test.go index 222aba1..6b78a13 100644 --- a/config/configuration_test.go +++ b/config/configuration_test.go @@ -27,7 +27,7 @@ func TestConfiguration_CreateInstance(t *testing.T) { assert.Equal(t, cldtest.CloudName, c.Cloud.CloudName) assert.Equal(t, signature.SHA256, c.Cloud.SignatureAlgorithm) - c, err = config.NewFromURL("") + _, err = config.NewFromURL("") if err == nil || err.Error() != "must provide CLOUDINARY_URL" { t.Error("Error expected, got: ", err) } diff --git a/gen/generate_setters/template.go b/gen/generate_setters/template.go index 0abfc8c..ba138fc 100644 --- a/gen/generate_setters/template.go +++ b/gen/generate_setters/template.go @@ -7,6 +7,7 @@ import ( "io" "strings" "text/template" + "unicode" ) // Generate template for setters file @@ -31,7 +32,7 @@ func ({{ .Receiver}} *{{ .StructName}}) <>(< Date: Tue, 25 Aug 2026 17:33:50 +0300 Subject: [PATCH 2/4] Bump gotestsum to v1.13.0 for Go 1.25+ compatibility v1.11.0 depends on golang.org/x/tools@v0.11.0, which fails to compile on Go 1.25 and 1.26 ("invalid array length -delta * delta" in internal/tokeninternal). v1.13.0 pulls x/tools@v0.36.0 and builds cleanly. Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 11605fe..1a6fead 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -34,7 +34,7 @@ jobs: echo "cloud_name: $(echo $CLOUDINARY_URL | cut -d'@' -f2)" - name: ๐Ÿงฐ Install gotestsum - run: go install gotest.tools/gotestsum@v1.11.0 + run: go install gotest.tools/gotestsum@v1.13.0 - name: ๐Ÿงช Run Tests run: | From ea7678f339aed5649e1ef4f448a78c7fbb2969d0 Mon Sep 17 00:00:00 2001 From: Constantine Nathanson Date: Tue, 25 Aug 2026 17:36:34 +0300 Subject: [PATCH 3/4] Fix unsigned underflow in memory watcher on Go 1.26 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `watchMemoryAllocation` computed `read - initialRead` on uint64 without checking that the live heap had actually grown. `runtime.MemStats.Alloc` is the live heap, so once the GC collects it can fall below the baseline taken before the upload; the subtraction then wraps to ~2^64 and trips the allocation limit. Go 1.26's GC timing makes this reproducible, which failed TestUploader_UploadLargeFile with "allocated 18446744073709509944 bytes". Only account for growth, and make `proceed`/`maxAlloc` atomic โ€” they were written by the returned closure and read by the sampling goroutine without synchronization. Co-Authored-By: Claude Opus 5 --- api/uploader/upload_asset_test.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/api/uploader/upload_asset_test.go b/api/uploader/upload_asset_test.go index 7967970..78174fb 100644 --- a/api/uploader/upload_asset_test.go +++ b/api/uploader/upload_asset_test.go @@ -8,6 +8,7 @@ import ( "os" "runtime" "strings" + "sync/atomic" "testing" "time" @@ -389,24 +390,29 @@ func readMemoryAlloc() uint64 { } func watchMemoryAllocation() func() uint64 { - var maxAlloc uint64 - proceed := true + var maxAlloc atomic.Uint64 + var proceed atomic.Bool + proceed.Store(true) initialRead := readMemoryAlloc() go func() { for i := 0; i < 10000; i++ { - if !proceed { + if !proceed.Load() { return } - read := readMemoryAlloc() - if read-initialRead > maxAlloc { - maxAlloc = read - initialRead + // Alloc is the live heap, which can drop below the baseline once the + // GC collects. Only consider growth, otherwise the unsigned + // subtraction underflows. + if read := readMemoryAlloc(); read > initialRead { + if grown := read - initialRead; grown > maxAlloc.Load() { + maxAlloc.Store(grown) + } } time.Sleep(time.Millisecond * 500) } }() return func() uint64 { - proceed = false - return maxAlloc + proceed.Store(false) + return maxAlloc.Load() } } From 8f27d135f55cc55cd9048d39ed6635966f937c0e Mon Sep 17 00:00:00 2001 From: Constantine Nathanson Date: Tue, 25 Aug 2026 17:42:39 +0300 Subject: [PATCH 4/4] Add Go 1.27 to the test matrix Go 1.27.0 is released; build, vet, and gotestsum v1.13.0 all verified clean against it locally. Collapse the README support columns into a "Go 1.24 - 1.27" range rather than adding a sixth column. Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yaml | 1 + README.md | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1a6fead..f092f9b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,6 +14,7 @@ jobs: - 1.24.x - 1.25.x - 1.26.x + - 1.27.x steps: - name: ๐Ÿ”„ Checkout Repository diff --git a/README.md b/README.md index 380fe01..64cca86 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,11 @@ For the complete documentation, see the [Go SDK Guide](https://cloudinary.com/do ## Version Support -| **SDK Version** | **Go 1.13 - 1.19** | **Go 1.20 - 1.23** | **Go 1.24** | **Go 1.25** | **Go 1.26** | -|-----------------|--------------------|--------------------|-------------|-------------|-------------| -| **2.8 & Up** | โŒ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | -| **2.7** | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | -| **1.x** | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | +| **SDK Version** | **Go 1.13 - 1.19** | **Go 1.20 - 1.23** | **Go 1.24 - 1.27** | +|-----------------|--------------------|--------------------|--------------------| +| **2.8 & Up** | โŒ | โœ”๏ธ | โœ”๏ธ | +| **2.7** | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | +| **1.x** | โœ”๏ธ | โœ”๏ธ | โœ”๏ธ | ## Installation