From 6905ec5c3e4361acdbb1eda50ebcb869330c0504 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:15:45 +0000 Subject: [PATCH 01/38] chore(internal): codegen related update --- .github/workflows/ci.yml | 27 +++++++++++++++++ client.go | 12 ++++++-- node.go | 3 ++ scripts/utils/upload-artifact.sh | 50 ++++++++++++++++++++++++++++++++ vm.go | 8 +++-- vmimage.go | 2 ++ vmscript.go | 2 ++ zone.go | 4 +++ 8 files changed, 103 insertions(+), 5 deletions(-) create mode 100755 scripts/utils/upload-artifact.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d36dd9f..173cbb5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,33 @@ on: - 'stl-preview-base/**' jobs: + build: + timeout-minutes: 10 + name: build + permissions: + contents: read + id-token: write + runs-on: ${{ github.repository == 'stainless-sdks/sfc-nodes-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: |- + github.repository == 'stainless-sdks/sfc-nodes-go' && + (github.event_name == 'push' || github.event.pull_request.head.repo.fork) + steps: + - uses: actions/checkout@v6 + + - name: Get GitHub OIDC Token + if: github.repository == 'stainless-sdks/sfc-nodes-go' + id: github-oidc + uses: actions/github-script@v8 + with: + script: core.setOutput('github_token', await core.getIDToken()); + + - name: Upload tarball + if: github.repository == 'stainless-sdks/sfc-nodes-go' + env: + URL: https://pkg.stainless.com/s + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + run: ./scripts/utils/upload-artifact.sh lint: timeout-minutes: 10 name: lint diff --git a/client.go b/client.go index 11b1c89..ee0c56f 100644 --- a/client.go +++ b/client.go @@ -17,9 +17,15 @@ import ( // directly, and instead use the [NewClient] method instead. type Client struct { Options []option.RequestOption - VMs VMService - Nodes NodeService - Zones ZoneService + // Manage your Virtual Machines. + VMs VMService + // Manage compute nodes. Create, list, extend, and release nodes for your + // workloads. + Nodes NodeService + // Zones represent physically colocated datacenters. Use these endpoints to + // discover available zones and their capacity, hardware specifications, and + // regional information. + Zones ZoneService } // DefaultClientOptions read from the environment (SFC_NODES_BEARER_TOKEN, diff --git a/node.go b/node.go index 1b8cdfd..3aed41f 100644 --- a/node.go +++ b/node.go @@ -20,6 +20,9 @@ import ( "github.com/sfcompute/nodes-go/packages/respjson" ) +// Manage compute nodes. Create, list, extend, and release nodes for your +// workloads. +// // NodeService contains methods and other services that help with interacting with // the sfc-nodes API. // diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh new file mode 100755 index 0000000..04e9894 --- /dev/null +++ b/scripts/utils/upload-artifact.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -exuo pipefail + +DIST_DIR="dist" +FILENAME="source.zip" + +mapfile -d '' files < <( + find . -type f \ + \( -name '*.go' -o -name 'go.mod' -o -name 'go.sum' \) \ + ! -path "./${DIST_DIR}/*" \ + -print0 +) + +if [[ ${#files[@]} -eq 0 ]]; then + echo -e "\033[31mNo Go source files found for packaging.\033[0m" + exit 1 +fi + +mkdir -p "$DIST_DIR" +rm -f "${DIST_DIR}/${FILENAME}" + +relative_files=() +for file in "${files[@]}"; do + relative_files+=("${file#./}") +done + +zip "${DIST_DIR}/${FILENAME}" "${relative_files[@]}" + +RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ + -H "Authorization: Bearer $AUTH" \ + -H "Content-Type: application/json") + +SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url') + +if [[ "$SIGNED_URL" == "null" ]]; then + echo -e "\033[31mFailed to get signed URL.\033[0m" + exit 1 +fi + +UPLOAD_RESPONSE=$(curl -v -X PUT \ + -H "Content-Type: application/zip" \ + --data-binary "@${DIST_DIR}/${FILENAME}" "$SIGNED_URL" 2>&1) + +if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then + echo -e "\033[32mUploaded build to Stainless storage.\033[0m" + echo -e "\033[32mInstallation: Download and unzip: 'https://pkg.stainless.com/s/sfc-nodes-go/$SHA'. Run 'go mod edit -replace github.com/sfcompute/nodes-go=/path/to/unzipped_directory'.\033[0m" +else + echo -e "\033[31mFailed to upload artifact.\033[0m" + exit 1 +fi diff --git a/vm.go b/vm.go index 1a516cb..8b9a8e8 100644 --- a/vm.go +++ b/vm.go @@ -16,6 +16,8 @@ import ( "github.com/sfcompute/nodes-go/packages/respjson" ) +// Manage your Virtual Machines. +// // VMService contains methods and other services that help with interacting with // the sfc-nodes API. // @@ -24,8 +26,10 @@ import ( // the [NewVMService] method instead. type VMService struct { Options []option.RequestOption - Script VMScriptService - Images VMImageService + // Manage your Virtual Machines. + Script VMScriptService + // Manage your Virtual Machines. + Images VMImageService } // NewVMService generates a new service that applies the given options to each diff --git a/vmimage.go b/vmimage.go index af390dd..21249b6 100644 --- a/vmimage.go +++ b/vmimage.go @@ -15,6 +15,8 @@ import ( "github.com/sfcompute/nodes-go/packages/respjson" ) +// Manage your Virtual Machines. +// // VMImageService contains methods and other services that help with interacting // with the sfc-nodes API. // diff --git a/vmscript.go b/vmscript.go index 4a79f9c..f57cc08 100644 --- a/vmscript.go +++ b/vmscript.go @@ -15,6 +15,8 @@ import ( "github.com/sfcompute/nodes-go/packages/respjson" ) +// Manage your Virtual Machines. +// // VMScriptService contains methods and other services that help with interacting // with the sfc-nodes API. // diff --git a/zone.go b/zone.go index 661b4d5..25d286d 100644 --- a/zone.go +++ b/zone.go @@ -15,6 +15,10 @@ import ( "github.com/sfcompute/nodes-go/packages/respjson" ) +// Zones represent physically colocated datacenters. Use these endpoints to +// discover available zones and their capacity, hardware specifications, and +// regional information. +// // ZoneService contains methods and other services that help with interacting with // the sfc-nodes API. // From 0efeacad5ecdbea83c326a17bb7a2f593afdcdcb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:52:49 +0000 Subject: [PATCH 02/38] chore(internal): codegen related update --- internal/requestconfig/requestconfig.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index d0ee6e7..cc3b68ff 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -355,11 +355,9 @@ func (b *bodyWithTimeout) Close() error { } func retryDelay(res *http.Response, retryCount int) time.Duration { - // If the API asks us to wait a certain amount of time (and it's a reasonable amount), - // just do what it says. - - if retryAfterDelay, ok := parseRetryAfterHeader(res); ok && 0 <= retryAfterDelay && retryAfterDelay < time.Minute { - return retryAfterDelay + // If the backend tells us to wait a certain amount of time, use that value + if retryAfterDelay, ok := parseRetryAfterHeader(res); ok { + return max(0, retryAfterDelay) } maxDelay := 8 * time.Second From d5f51e8e5330f29e7c05a58fea234a90d3c28a86 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 17:59:00 +0000 Subject: [PATCH 03/38] chore(ci): skip uploading artifacts on stainless-internal branches --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 173cbb5..11598ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,14 +27,18 @@ jobs: - uses: actions/checkout@v6 - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/sfc-nodes-go' + if: |- + github.repository == 'stainless-sdks/sfc-nodes-go' && + !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Upload tarball - if: github.repository == 'stainless-sdks/sfc-nodes-go' + if: |- + github.repository == 'stainless-sdks/sfc-nodes-go' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} From d4332e4aeae3e5a569fc586ab430182cb705de17 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 08:32:51 +0000 Subject: [PATCH 04/38] chore(internal): minor cleanup --- client_test.go | 2 +- internal/apiform/form_test.go | 9 ++++++--- internal/apiform/tag.go | 2 ++ internal/apijson/decoder.go | 9 +++------ internal/apijson/encoder.go | 21 --------------------- internal/apijson/enum.go | 9 ++------- internal/apijson/json_test.go | 6 +++--- internal/apijson/tag.go | 2 ++ internal/apiquery/encoder.go | 23 +---------------------- internal/requestconfig/requestconfig.go | 6 +++--- usage_test.go | 2 +- 11 files changed, 24 insertions(+), 67 deletions(-) diff --git a/client_test.go b/client_test.go index eebfb89..cbb7b67 100644 --- a/client_test.go +++ b/client_test.go @@ -38,7 +38,7 @@ func TestUserAgentHeader(t *testing.T) { }, }), ) - client.Nodes.List(context.Background(), sfcnodes.NodeListParams{}) + _, _ = client.Nodes.List(context.Background(), sfcnodes.NodeListParams{}) if userAgent != fmt.Sprintf("SFCNodes/Go %s", internal.PackageVersion) { t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent) } diff --git a/internal/apiform/form_test.go b/internal/apiform/form_test.go index b94c1ec..f5e405d 100644 --- a/internal/apiform/form_test.go +++ b/internal/apiform/form_test.go @@ -585,14 +585,17 @@ func TestEncode(t *testing.T) { t.Run(name, func(t *testing.T) { buf := bytes.NewBuffer(nil) writer := multipart.NewWriter(buf) - writer.SetBoundary("xxx") + err := writer.SetBoundary("xxx") + if err != nil { + t.Errorf("setting boundary for %v failed with error %v", test.val, err) + } - var arrayFmt string = "indices:dots" + arrayFmt := "indices:dots" if tags := strings.Split(name, ","); len(tags) > 1 { arrayFmt = tags[1] } - err := MarshalWithSettings(test.val, writer, arrayFmt) + err = MarshalWithSettings(test.val, writer, arrayFmt) if err != nil { t.Errorf("serialization of %v failed with error %v", test.val, err) } diff --git a/internal/apiform/tag.go b/internal/apiform/tag.go index b353617..5dd0b45 100644 --- a/internal/apiform/tag.go +++ b/internal/apiform/tag.go @@ -60,6 +60,8 @@ func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { tag.extras = true case "required": tag.required = true + case "metadata": + tag.metadata = true } } } diff --git a/internal/apijson/decoder.go b/internal/apijson/decoder.go index b426199..8c869f9 100644 --- a/internal/apijson/decoder.go +++ b/internal/apijson/decoder.go @@ -393,7 +393,7 @@ func (d *decoderBuilder) newStructTypeDecoder(t reflect.Type) decoderFunc { for _, decoder := range anonymousDecoders { // ignore errors - decoder.fn(node, value.FieldByIndex(decoder.idx), state) + _ = decoder.fn(node, value.FieldByIndex(decoder.idx), state) } for _, inlineDecoder := range inlineDecoders { @@ -462,7 +462,7 @@ func (d *decoderBuilder) newStructTypeDecoder(t reflect.Type) decoderFunc { // Handle null [param.Opt] if itemNode.Type == gjson.Null && dest.IsValid() && dest.Type().Implements(reflect.TypeOf((*param.Optional)(nil)).Elem()) { - dest.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(itemNode.Raw)) + _ = dest.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(itemNode.Raw)) continue } @@ -684,8 +684,5 @@ func guardUnknown(state *decoderState, v reflect.Value) bool { constantString, ok := v.Interface().(interface{ Default() string }) named := v.Type() != stringType - if guardStrict(state, ok && named && v.Equal(reflect.ValueOf(constantString.Default()))) { - return true - } - return false + return guardStrict(state, ok && named && v.Equal(reflect.ValueOf(constantString.Default()))) } diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go index ab7a3c1..f4e3a5c 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -290,27 +290,6 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { } } -func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc { - f, _ := t.FieldByName("Value") - enc := e.typeEncoder(f.Type) - - return func(value reflect.Value) (json []byte, err error) { - present := value.FieldByName("Present") - if !present.Bool() { - return nil, nil - } - null := value.FieldByName("Null") - if null.Bool() { - return []byte("null"), nil - } - raw := value.FieldByName("Raw") - if !raw.IsNil() { - return e.typeEncoder(raw.Type())(raw) - } - return enc(value.FieldByName("Value")) - } -} - func (e *encoder) newTimeTypeEncoder() encoderFunc { format := e.dateFormat return func(value reflect.Value) (json []byte, err error) { diff --git a/internal/apijson/enum.go b/internal/apijson/enum.go index 5bef11c..a1626a5 100644 --- a/internal/apijson/enum.go +++ b/internal/apijson/enum.go @@ -4,7 +4,6 @@ import ( "fmt" "reflect" "slices" - "sync" "github.com/tidwall/gjson" ) @@ -15,7 +14,6 @@ import ( type validationEntry struct { field reflect.StructField - required bool legalValues struct { strings []string // 1 represents true, 0 represents false, -1 represents either @@ -24,9 +22,6 @@ type validationEntry struct { } } -type validatorFunc func(reflect.Value) exactness - -var validators sync.Map var validationRegistry = map[reflect.Type][]validationEntry{} func RegisterFieldValidator[T any, V string | bool | int | float64](fieldName string, values ...V) { @@ -111,9 +106,9 @@ func (state *decoderState) validateBool(v reflect.Value) { return } b := v.Bool() - if state.validator.legalValues.bools == 1 && b == false { + if state.validator.legalValues.bools == 1 && !b { state.exactness = loose - } else if state.validator.legalValues.bools == 0 && b == true { + } else if state.validator.legalValues.bools == 0 && b { state.exactness = loose } } diff --git a/internal/apijson/json_test.go b/internal/apijson/json_test.go index fac9fcc..6932a7b 100644 --- a/internal/apijson/json_test.go +++ b/internal/apijson/json_test.go @@ -87,7 +87,7 @@ type JSONFieldStruct struct { C string `json:"c"` D string `json:"d"` ExtraFields map[string]int64 `json:"" api:"extrafields"` - JSON JSONFieldStructJSON `json:",metadata"` + JSON JSONFieldStructJSON `json:"-" api:"metadata"` } type JSONFieldStructJSON struct { @@ -113,12 +113,12 @@ type Union interface { type Inline struct { InlineField Primitives `json:",inline"` - JSON InlineJSON `json:",metadata"` + JSON InlineJSON `json:"-" api:"metadata"` } type InlineArray struct { InlineField []string `json:",inline"` - JSON InlineJSON `json:",metadata"` + JSON InlineJSON `json:"-" api:"metadata"` } type InlineJSON struct { diff --git a/internal/apijson/tag.go b/internal/apijson/tag.go index 49731b8..0511d69 100644 --- a/internal/apijson/tag.go +++ b/internal/apijson/tag.go @@ -57,6 +57,8 @@ func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { tag.extras = true case "required": tag.required = true + case "metadata": + tag.metadata = true } } } diff --git a/internal/apiquery/encoder.go b/internal/apiquery/encoder.go index 96e583f..87c13d3 100644 --- a/internal/apiquery/encoder.go +++ b/internal/apiquery/encoder.go @@ -193,7 +193,7 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { return func(key string, value reflect.Value) (pairs []Pair, err error) { for _, ef := range encoderFields { - var subkey string = e.renderKeyPath(key, ef.tag.name) + subkey := e.renderKeyPath(key, ef.tag.name) if ef.tag.inline { subkey = key } @@ -372,27 +372,6 @@ func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { } } -func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc { - f, _ := t.FieldByName("Value") - enc := e.typeEncoder(f.Type) - - return func(key string, value reflect.Value) ([]Pair, error) { - present := value.FieldByName("Present") - if !present.Bool() { - return nil, nil - } - null := value.FieldByName("Null") - if null.Bool() { - return nil, fmt.Errorf("apiquery: field cannot be null") - } - raw := value.FieldByName("Raw") - if !raw.IsNil() { - return e.typeEncoder(raw.Type())(key, raw) - } - return enc(key, value.FieldByName("Value")) - } -} - func (e *encoder) newTimeTypeEncoder(_ reflect.Type) encoderFunc { format := e.dateFormat return func(key string, value reflect.Value) ([]Pair, error) { diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index cc3b68ff..f641524 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -461,7 +461,7 @@ func (cfg *RequestConfig) Execute() (err error) { // Close the response body before retrying to prevent connection leaks if res != nil && res.Body != nil { - res.Body.Close() + _ = res.Body.Close() } select { @@ -489,7 +489,7 @@ func (cfg *RequestConfig) Execute() (err error) { if res.StatusCode >= 400 { contents, err := io.ReadAll(res.Body) - res.Body.Close() + _ = res.Body.Close() if err != nil { return err } @@ -520,7 +520,7 @@ func (cfg *RequestConfig) Execute() (err error) { } contents, err := io.ReadAll(res.Body) - res.Body.Close() + _ = res.Body.Close() if err != nil { return fmt.Errorf("error reading response body: %w", err) } diff --git a/usage_test.go b/usage_test.go index a25bf6d..2f7f230 100644 --- a/usage_test.go +++ b/usage_test.go @@ -13,6 +13,7 @@ import ( ) func TestUsage(t *testing.T) { + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -24,7 +25,6 @@ func TestUsage(t *testing.T) { option.WithBaseURL(baseURL), option.WithBearerToken("My Bearer Token"), ) - t.Skip("Mock server tests are disabled") listResponseNode, err := client.Nodes.List(context.TODO(), sfcnodes.NodeListParams{}) if err != nil { t.Fatalf("err should be nil: %s", err.Error()) From f740ed8beb5a3e52d0c879e31a9ed47f85af233d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 08:52:50 +0000 Subject: [PATCH 05/38] chore(internal): use explicit returns --- node.go | 24 ++++++++++++------------ vm.go | 4 ++-- vmimage.go | 6 +++--- vmscript.go | 4 ++-- zone.go | 6 +++--- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/node.go b/node.go index 3aed41f..b638877 100644 --- a/node.go +++ b/node.go @@ -47,7 +47,7 @@ func (r *NodeService) New(ctx context.Context, body NodeNewParams, opts ...optio opts = slices.Concat(r.Options, opts) path := "v1/nodes" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // List all nodes for the authenticated account @@ -55,7 +55,7 @@ func (r *NodeService) List(ctx context.Context, query NodeListParams, opts ...op opts = slices.Concat(r.Options, opts) path := "v1/nodes" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return + return res, err } // Delete a node by id. The node cannot be deleted if it has active or pending VMs. @@ -64,11 +64,11 @@ func (r *NodeService) Delete(ctx context.Context, id string, opts ...option.Requ opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) if id == "" { err = errors.New("missing required id parameter") - return + return err } path := fmt.Sprintf("v1/nodes/%s", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) - return + return err } // Purchase additional time to extend the end time of a reserved VM node @@ -76,11 +76,11 @@ func (r *NodeService) Extend(ctx context.Context, id string, body NodeExtendPara opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("v1/nodes/%s/extend", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPatch, path, body, &res, opts...) - return + return res, err } // Retrieve details of a specific node by its ID or name @@ -88,11 +88,11 @@ func (r *NodeService) Get(ctx context.Context, id string, opts ...option.Request opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("v1/nodes/%s", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } // Redeploy a node by replacing its current VM with a new one. Optionally update @@ -101,11 +101,11 @@ func (r *NodeService) Redeploy(ctx context.Context, id string, body NodeRedeploy opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("v1/nodes/%s/redeploy", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPut, path, body, &res, opts...) - return + return res, err } // Release an auto reserved VM node from its procurement, reducing the @@ -114,11 +114,11 @@ func (r *NodeService) Release(ctx context.Context, id string, opts ...option.Req opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("v1/nodes/%s/release", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPatch, path, nil, &res, opts...) - return + return res, err } type AcceleratorType string diff --git a/vm.go b/vm.go index 8b9a8e8..93ababb 100644 --- a/vm.go +++ b/vm.go @@ -47,14 +47,14 @@ func (r *VMService) Logs(ctx context.Context, query VMLogsParams, opts ...option opts = slices.Concat(r.Options, opts) path := "v0/vms/logs2" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return + return res, err } func (r *VMService) SSH(ctx context.Context, query VMSSHParams, opts ...option.RequestOption) (res *VmsshResponse, err error) { opts = slices.Concat(r.Options, opts) path := "v0/vms/ssh" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return + return res, err } type VMLogsResponse struct { diff --git a/vmimage.go b/vmimage.go index 21249b6..2a58cf3 100644 --- a/vmimage.go +++ b/vmimage.go @@ -41,7 +41,7 @@ func (r *VMImageService) List(ctx context.Context, opts ...option.RequestOption) opts = slices.Concat(r.Options, opts) path := "v1/vms/images" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } // Get the download URL for a VM image by ID @@ -49,11 +49,11 @@ func (r *VMImageService) Get(ctx context.Context, imageID string, opts ...option opts = slices.Concat(r.Options, opts) if imageID == "" { err = errors.New("missing required image_id parameter") - return + return nil, err } path := fmt.Sprintf("v1/vms/images/%s", imageID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } // Response body for listing images diff --git a/vmscript.go b/vmscript.go index f57cc08..4ff1ad8 100644 --- a/vmscript.go +++ b/vmscript.go @@ -40,14 +40,14 @@ func (r *VMScriptService) New(ctx context.Context, body VMScriptNewParams, opts opts = slices.Concat(r.Options, opts) path := "v0/vms/script" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } func (r *VMScriptService) Get(ctx context.Context, opts ...option.RequestOption) (res *VMScriptGetResponse, err error) { opts = slices.Concat(r.Options, opts) path := "v0/vms/script" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } // UserDataUnion contains all possible properties and values from [string], diff --git a/zone.go b/zone.go index 25d286d..66df62e 100644 --- a/zone.go +++ b/zone.go @@ -43,7 +43,7 @@ func (r *ZoneService) List(ctx context.Context, opts ...option.RequestOption) (r opts = slices.Concat(r.Options, opts) path := "v0/zones" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } // Get detailed information about a specific zone @@ -51,11 +51,11 @@ func (r *ZoneService) Get(ctx context.Context, id string, opts ...option.Request opts = slices.Concat(r.Options, opts) if id == "" { err = errors.New("missing required id parameter") - return + return nil, err } path := fmt.Sprintf("v0/zones/%s", id) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } type ZoneListResponse struct { From f8f3570dcb58aaecc9139b4bf4e93c1a77f4a711 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 08:57:53 +0000 Subject: [PATCH 06/38] chore(internal): use explicit returns in more places --- internal/apiform/encoder.go | 2 +- internal/apiform/tag.go | 6 +++--- internal/apijson/encoder.go | 2 +- internal/apijson/json_test.go | 2 +- internal/apijson/tag.go | 6 +++--- internal/apiquery/encoder.go | 10 +++++----- internal/apiquery/tag.go | 6 +++--- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index cd7b726..8ad4835 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -469,5 +469,5 @@ func WriteExtras(writer *multipart.Writer, extras map[string]any) (err error) { break } } - return + return err } diff --git a/internal/apiform/tag.go b/internal/apiform/tag.go index 5dd0b45..d9915d4 100644 --- a/internal/apiform/tag.go +++ b/internal/apiform/tag.go @@ -24,7 +24,7 @@ func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool raw, ok = field.Tag.Lookup(jsonStructTag) } if !ok { - return + return tag, ok } parts := strings.Split(raw, ",") if len(parts) == 0 { @@ -45,7 +45,7 @@ func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool } parseApiStructTag(field, &tag) - return + return tag, ok } func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { @@ -68,5 +68,5 @@ func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { format, ok = field.Tag.Lookup(formatStructTag) - return + return format, ok } diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go index f4e3a5c..0decb73 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -286,7 +286,7 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { return nil, err } } - return + return json, err } } diff --git a/internal/apijson/json_test.go b/internal/apijson/json_test.go index 6932a7b..19b3614 100644 --- a/internal/apijson/json_test.go +++ b/internal/apijson/json_test.go @@ -268,7 +268,7 @@ type MarshallingUnionStruct struct { func (r *MarshallingUnionStruct) UnmarshalJSON(data []byte) (err error) { *r = MarshallingUnionStruct{} err = UnmarshalRoot(data, &r.Union) - return + return err } func (r MarshallingUnionStruct) MarshalJSON() (data []byte, err error) { diff --git a/internal/apijson/tag.go b/internal/apijson/tag.go index 0511d69..17b2130 100644 --- a/internal/apijson/tag.go +++ b/internal/apijson/tag.go @@ -20,7 +20,7 @@ type parsedStructTag struct { func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { raw, ok := field.Tag.Lookup(jsonStructTag) if !ok { - return + return tag, ok } parts := strings.Split(raw, ",") if len(parts) == 0 { @@ -42,7 +42,7 @@ func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool // the `api` struct tag is only used alongside `json` for custom behaviour parseApiStructTag(field, &tag) - return + return tag, ok } func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { @@ -65,5 +65,5 @@ func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { format, ok = field.Tag.Lookup(formatStructTag) - return + return format, ok } diff --git a/internal/apiquery/encoder.go b/internal/apiquery/encoder.go index 87c13d3..d75a89b 100644 --- a/internal/apiquery/encoder.go +++ b/internal/apiquery/encoder.go @@ -103,7 +103,7 @@ func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc { encoder := e.typeEncoder(t.Elem()) return func(key string, value reflect.Value) (pairs []Pair, err error) { if !value.IsValid() || value.IsNil() { - return + return pairs, err } return encoder(key, value.Elem()) } @@ -205,7 +205,7 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { } pairs = append(pairs, subpairs...) } - return + return pairs, err } } @@ -256,7 +256,7 @@ func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc { } pairs = append(pairs, subpairs...) } - return + return pairs, err } } @@ -300,7 +300,7 @@ func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { } pairs = append(pairs, subpairs...) } - return + return pairs, err } case ArrayQueryFormatIndices: panic("The array indices format is not supported yet") @@ -315,7 +315,7 @@ func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { } pairs = append(pairs, subpairs...) } - return + return pairs, err } default: panic(fmt.Sprintf("Unknown ArrayFormat value: %d", e.settings.ArrayFormat)) diff --git a/internal/apiquery/tag.go b/internal/apiquery/tag.go index 772c40e..9e413ad 100644 --- a/internal/apiquery/tag.go +++ b/internal/apiquery/tag.go @@ -18,7 +18,7 @@ type parsedStructTag struct { func parseQueryStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { raw, ok := field.Tag.Lookup(queryStructTag) if !ok { - return + return tag, ok } parts := strings.Split(raw, ",") if len(parts) == 0 { @@ -35,10 +35,10 @@ func parseQueryStructTag(field reflect.StructField) (tag parsedStructTag, ok boo tag.inline = true } } - return + return tag, ok } func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { format, ok = field.Tag.Lookup(formatStructTag) - return + return format, ok } From f1ab27477b86808c8942b6ce899803ac150c8b16 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:35:46 +0000 Subject: [PATCH 07/38] chore(internal): tweak CI branches --- .github/workflows/ci.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11598ba..ce43bc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' From c9973115ba8dcfb4eb10f7cab9c438c7729779fb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 04:37:13 +0000 Subject: [PATCH 08/38] chore(internal): update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c6d0501..8554aff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log codegen.log Brewfile.lock.json .idea/ From 59072fa7b6eda168b36e5702c27e85cd99ab84cc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 03:13:01 +0000 Subject: [PATCH 09/38] chore(ci): skip lint on metadata-only changes Note that we still want to run tests, as these depend on the metadata. --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce43bc0..4fab5c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,8 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/sfc-nodes-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: |- github.repository == 'stainless-sdks/sfc-nodes-go' && - (github.event_name == 'push' || github.event.pull_request.head.repo.fork) + (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && + (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 From 9be56edd00c8e78ee7d3cd56604eeebe63df376b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 04:00:19 +0000 Subject: [PATCH 10/38] chore(internal): support default value struct tag --- internal/apiform/tag.go | 26 +++++++++++++++++++++----- internal/apijson/encoder.go | 8 ++++++++ internal/apijson/json_test.go | 17 +++++++++++++++++ internal/apijson/tag.go | 26 +++++++++++++++++++++----- 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/internal/apiform/tag.go b/internal/apiform/tag.go index d9915d4..f0c9d14 100644 --- a/internal/apiform/tag.go +++ b/internal/apiform/tag.go @@ -9,13 +9,15 @@ const apiStructTag = "api" const jsonStructTag = "json" const formStructTag = "form" const formatStructTag = "format" +const defaultStructTag = "default" type parsedStructTag struct { - name string - required bool - extras bool - metadata bool - omitzero bool + name string + required bool + extras bool + metadata bool + omitzero bool + defaultValue any } func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { @@ -45,9 +47,23 @@ func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool } parseApiStructTag(field, &tag) + parseDefaultStructTag(field, &tag) return tag, ok } +func parseDefaultStructTag(field reflect.StructField, tag *parsedStructTag) { + if field.Type.Kind() != reflect.String { + // Only strings are currently supported + return + } + + raw, ok := field.Tag.Lookup(defaultStructTag) + if !ok { + return + } + tag.defaultValue = raw +} + func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { raw, ok := field.Tag.Lookup(apiStructTag) if !ok { diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go index 0decb73..a601291 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -12,6 +12,8 @@ import ( "time" "github.com/tidwall/sjson" + + shimjson "github.com/sfcompute/nodes-go/internal/encoding/json" ) var encoders sync.Map // map[encoderEntry]encoderFunc @@ -271,6 +273,12 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { if err != nil { return nil, err } + if ef.tag.defaultValue != nil && (!field.IsValid() || field.IsZero()) { + encoded, err = shimjson.Marshal(ef.tag.defaultValue) + if err != nil { + return nil, err + } + } if encoded == nil { continue } diff --git a/internal/apijson/json_test.go b/internal/apijson/json_test.go index 19b3614..2853bf9 100644 --- a/internal/apijson/json_test.go +++ b/internal/apijson/json_test.go @@ -614,3 +614,20 @@ func TestEncode(t *testing.T) { }) } } + +type StructWithDefault struct { + Type string `json:"type" default:"foo"` +} + +func TestDefault(t *testing.T) { + value := StructWithDefault{} + expected := `{"type":"foo"}` + + raw, err := Marshal(value) + if err != nil { + t.Fatalf("serialization of %v failed with error %v", value, err) + } + if string(raw) != expected { + t.Fatalf("expected %+#v to serialize to %s but got %s", value, expected, string(raw)) + } +} diff --git a/internal/apijson/tag.go b/internal/apijson/tag.go index 17b2130..efcaf8c 100644 --- a/internal/apijson/tag.go +++ b/internal/apijson/tag.go @@ -8,13 +8,15 @@ import ( const apiStructTag = "api" const jsonStructTag = "json" const formatStructTag = "format" +const defaultStructTag = "default" type parsedStructTag struct { - name string - required bool - extras bool - metadata bool - inline bool + name string + required bool + extras bool + metadata bool + inline bool + defaultValue any } func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { @@ -42,9 +44,23 @@ func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool // the `api` struct tag is only used alongside `json` for custom behaviour parseApiStructTag(field, &tag) + parseDefaultStructTag(field, &tag) return tag, ok } +func parseDefaultStructTag(field reflect.StructField, tag *parsedStructTag) { + if field.Type.Kind() != reflect.String { + // Only strings are currently supported + return + } + + raw, ok := field.Tag.Lookup(defaultStructTag) + if !ok { + return + } + tag.defaultValue = raw +} + func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { raw, ok := field.Tag.Lookup(apiStructTag) if !ok { From 68d8f8e87b235485f65c079a52cd3c381d9936be Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 04:03:07 +0000 Subject: [PATCH 11/38] chore(client): fix multipart serialisation of Default() fields --- internal/apiform/encoder.go | 8 ++++++++ internal/apiform/form_test.go | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index 8ad4835..bbe2001 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -265,6 +265,14 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { } return typeEncoderFn(key, value, writer) } + } else if ptag.defaultValue != nil { + typeEncoderFn := e.typeEncoder(field.Type) + encoderFn = func(key string, value reflect.Value, writer *multipart.Writer) error { + if value.IsZero() { + return typeEncoderFn(key, reflect.ValueOf(ptag.defaultValue), writer) + } + return typeEncoderFn(key, value, writer) + } } else { encoderFn = e.typeEncoder(field.Type) } diff --git a/internal/apiform/form_test.go b/internal/apiform/form_test.go index f5e405d..9f1d6cc 100644 --- a/internal/apiform/form_test.go +++ b/internal/apiform/form_test.go @@ -123,6 +123,11 @@ type StructUnion struct { param.APIUnion } +type ConstantStruct struct { + Anchor string `form:"anchor" default:"created_at"` + Seconds int `form:"seconds"` +} + type MultipartMarshalerParent struct { Middle MultipartMarshalerMiddleNext `form:"middle"` } @@ -554,6 +559,37 @@ Content-Disposition: form-data; name="union" Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)), }, }, + "constant_zero_value": { + `--xxx +Content-Disposition: form-data; name="anchor" + +created_at +--xxx +Content-Disposition: form-data; name="seconds" + +3600 +--xxx-- +`, + ConstantStruct{ + Seconds: 3600, + }, + }, + "constant_explicit_value": { + `--xxx +Content-Disposition: form-data; name="anchor" + +created_at_override +--xxx +Content-Disposition: form-data; name="seconds" + +3600 +--xxx-- +`, + ConstantStruct{ + Anchor: "created_at_override", + Seconds: 3600, + }, + }, "deeply-nested-struct,brackets": { `--xxx Content-Disposition: form-data; name="middle[middleNext][child]" From 179abd873e3776a78b5f465b2878981f684c0001 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 05:53:14 +0000 Subject: [PATCH 12/38] fix: prevent duplicate ? in query params --- internal/requestconfig/requestconfig.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index f641524..04dce97 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -121,7 +121,16 @@ func NewRequestConfig(ctx context.Context, method string, u string, body any, ds } params := q.Encode() if params != "" { - u = u + "?" + params + parsed, err := url.Parse(u) + if err != nil { + return nil, err + } + if parsed.RawQuery != "" { + parsed.RawQuery = parsed.RawQuery + "&" + params + u = parsed.String() + } else { + u = u + "?" + params + } } } if body, ok := body.([]byte); ok { From 9a31c22ba410290e150a66b0f7bec72c9ae131be Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 05:56:41 +0000 Subject: [PATCH 13/38] chore: remove unnecessary error check for url parsing --- internal/requestconfig/requestconfig.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index 04dce97..1f8279c 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -121,10 +121,7 @@ func NewRequestConfig(ctx context.Context, method string, u string, body any, ds } params := q.Encode() if params != "" { - parsed, err := url.Parse(u) - if err != nil { - return nil, err - } + parsed, _ := url.Parse(u) if parsed.RawQuery != "" { parsed.RawQuery = parsed.RawQuery + "&" + params u = parsed.String() From ecfc0ceb5a993acb87fc73731ba19c0e24349ea9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 05:57:35 +0000 Subject: [PATCH 14/38] feat(internal): support comma format in multipart form encoding --- internal/apiform/encoder.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index bbe2001..960732e 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -183,6 +183,18 @@ func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { itemEncoder := e.typeEncoder(t.Elem()) keyFn := e.arrayKeyEncoder() + if e.arrayFmt == "comma" { + return func(key string, v reflect.Value, writer *multipart.Writer) error { + if v.Len() == 0 { + return nil + } + elements := make([]string, v.Len()) + for i := 0; i < v.Len(); i++ { + elements[i] = fmt.Sprint(v.Index(i).Interface()) + } + return writer.WriteField(key, strings.Join(elements, ",")) + } + } return func(key string, v reflect.Value, writer *multipart.Writer) error { if keyFn == nil { return fmt.Errorf("apiform: unsupported array format") From 046b6e1a7cb29da1c78c90944644f50e5629f304 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 06:34:13 +0000 Subject: [PATCH 15/38] chore(ci): support opting out of skipping builds on metadata-only commits --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fab5c4..d36933f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,8 +24,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/sfc-nodes-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: |- github.repository == 'stainless-sdks/sfc-nodes-go' && - (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && - (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') + (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 From afd6c27ac6b3ae51794c4098293b9f1a676cf2a2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 06:40:04 +0000 Subject: [PATCH 16/38] chore: update docs for api:"required" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d65833..e13453c 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ func main() { The sfcnodes library uses the [`omitzero`](https://tip.golang.org/doc/go1.24#encodingjsonpkgencodingjson) semantics from the Go 1.24+ `encoding/json` release for request fields. -Required primitive fields (`int64`, `string`, etc.) feature the tag \`json:"...,required"\`. These +Required primitive fields (`int64`, `string`, etc.) feature the tag \`api:"required"\`. These fields are always serialized, even their zero values. Optional primitive types are wrapped in a `param.Opt[T]`. These fields can be set with the provided constructors, `sfcnodes.String(string)`, `sfcnodes.Int(int64)`, etc. From fa94f5f06356b76992a572891c762c3fe3559dc0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 07:54:26 +0000 Subject: [PATCH 17/38] fix: fix issue with unmarshaling in some cases --- node.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/node.go b/node.go index b638877..2f82f50 100644 --- a/node.go +++ b/node.go @@ -4,7 +4,6 @@ package sfcnodes import ( "context" - "encoding/json" "errors" "fmt" "net/http" @@ -522,7 +521,7 @@ func (r NodeNewParams) MarshalJSON() (data []byte, err error) { return shimjson.Marshal(r.CreateNodesRequest) } func (r *NodeNewParams) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, &r.CreateNodesRequest) + return apijson.UnmarshalRoot(data, r) } type NodeListParams struct { @@ -556,7 +555,7 @@ func (r NodeExtendParams) MarshalJSON() (data []byte, err error) { return shimjson.Marshal(r.ExtendNodeRequest) } func (r *NodeExtendParams) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, &r.ExtendNodeRequest) + return apijson.UnmarshalRoot(data, r) } type NodeRedeployParams struct { From 0b4c0ecae518042d6ceecb8525718fb141e3e1c8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:34:59 +0000 Subject: [PATCH 18/38] fix: better respect format tags from the spec --- node.go | 2 +- vm.go | 16 ++++++++-------- vmimage.go | 2 +- vmscript.go | 6 +++--- zone.go | 4 ++-- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/node.go b/node.go index 2f82f50..9b5d5fa 100644 --- a/node.go +++ b/node.go @@ -129,7 +129,7 @@ const ( // The properties DesiredCount, MaxPricePerNodeHour are required. type CreateNodesRequestParam struct { - DesiredCount int64 `json:"desired_count" api:"required"` + DesiredCount int64 `json:"desired_count" api:"required" format:"u-int32"` // Max price per hour for a node in cents MaxPricePerNodeHour int64 `json:"max_price_per_node_hour" api:"required"` // End time as Unix timestamp in seconds If provided, end time must be aligned to diff --git a/vm.go b/vm.go index 93ababb..53d3ff9 100644 --- a/vm.go +++ b/vm.go @@ -74,13 +74,13 @@ func (r *VMLogsResponse) UnmarshalJSON(data []byte) error { } type VMLogsResponseData struct { - Data []int64 `json:"data" api:"required"` + Data []int64 `json:"data" api:"required" format:"u-int8"` InstanceID string `json:"instance_id" api:"required"` - MonotonicTimestampNanoSec int64 `json:"monotonic_timestamp_nano_sec" api:"required"` - MonotonicTimestampSec int64 `json:"monotonic_timestamp_sec" api:"required"` + MonotonicTimestampNanoSec int64 `json:"monotonic_timestamp_nano_sec" api:"required" format:"u-int32"` + MonotonicTimestampSec int64 `json:"monotonic_timestamp_sec" api:"required" format:"u-int64"` // In RFC 3339 format RealtimeTimestamp string `json:"realtime_timestamp" api:"required"` - Seqnum int64 `json:"seqnum" api:"required"` + Seqnum int64 `json:"seqnum" api:"required" format:"u-int64"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { Data respjson.Field @@ -102,7 +102,7 @@ func (r *VMLogsResponseData) UnmarshalJSON(data []byte) error { type VmsshResponse struct { SSHHostname string `json:"ssh_hostname" api:"required"` - SSHPort int64 `json:"ssh_port" api:"required"` + SSHPort int64 `json:"ssh_port" api:"required" format:"u-int16"` // Unix timestamp. LastAttemptedKeyUpdate int64 `json:"last_attempted_key_update" api:"nullable"` // Unix timestamp. @@ -149,10 +149,10 @@ type VMLogsParams struct { // Any of "seqnum_asc", "seqnum_desc". OrderBy VMLogsParamsOrderBy `query:"order_by,omitzero" api:"required" json:"-"` BeforeRealtimeTimestamp param.Opt[string] `query:"before_realtime_timestamp,omitzero" json:"-"` - BeforeSeqnum param.Opt[int64] `query:"before_seqnum,omitzero" json:"-"` - Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` + BeforeSeqnum param.Opt[int64] `query:"before_seqnum,omitzero" format:"u-int64" json:"-"` + Limit param.Opt[int64] `query:"limit,omitzero" format:"u-int64" json:"-"` SinceRealtimeTimestamp param.Opt[string] `query:"since_realtime_timestamp,omitzero" json:"-"` - SinceSeqnum param.Opt[int64] `query:"since_seqnum,omitzero" json:"-"` + SinceSeqnum param.Opt[int64] `query:"since_seqnum,omitzero" format:"u-int64" json:"-"` paramObj } diff --git a/vmimage.go b/vmimage.go index 2a58cf3..25966b4 100644 --- a/vmimage.go +++ b/vmimage.go @@ -130,7 +130,7 @@ type VMImageGetResponse struct { // Any of "image". Object VMImageGetResponseObject `json:"object" api:"required"` // Size of the image file in bytes - ObjectSize int64 `json:"object_size" api:"required"` + ObjectSize int64 `json:"object_size" api:"required" format:"u-int64"` // SHA256 hash of the image file for integrity verification Sha256Hash string `json:"sha256_hash" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. diff --git a/vmscript.go b/vmscript.go index 4ff1ad8..041be52 100644 --- a/vmscript.go +++ b/vmscript.go @@ -123,7 +123,7 @@ func (u *UserDataUnionParam) asAny() any { type VMScriptNewResponse struct { // if the script is valid utf8 then the response may be in either string, or byte // form and the client must handle both - Script UserDataUnion `json:"script" api:"required"` + Script UserDataUnion `json:"script" api:"required" format:"u-int8"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { Script respjson.Field @@ -141,7 +141,7 @@ func (r *VMScriptNewResponse) UnmarshalJSON(data []byte) error { type VMScriptGetResponse struct { // if the script is valid utf8 then the response may be in either string, or byte // form and the client must handle both - Script UserDataUnion `json:"script" api:"required"` + Script UserDataUnion `json:"script" api:"required" format:"u-int8"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { Script respjson.Field @@ -159,7 +159,7 @@ func (r *VMScriptGetResponse) UnmarshalJSON(data []byte) error { type VMScriptNewParams struct { // if the script is valid utf8 then the response may be in either string, or byte // form and the client must handle both - Script UserDataUnionParam `json:"script,omitzero" api:"required"` + Script UserDataUnionParam `json:"script,omitzero" api:"required" format:"u-int8"` paramObj } diff --git a/zone.go b/zone.go index 66df62e..7d522e8 100644 --- a/zone.go +++ b/zone.go @@ -117,7 +117,7 @@ type ZoneListResponseDataAvailableCapacity struct { // Unix timestamp. EndTimestamp int64 `json:"end_timestamp" api:"required"` // The number of nodes available during this time period - Quantity int64 `json:"quantity" api:"required"` + Quantity int64 `json:"quantity" api:"required" format:"u-int64"` // Unix timestamp. StartTimestamp int64 `json:"start_timestamp" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. @@ -177,7 +177,7 @@ type ZoneGetResponseAvailableCapacity struct { // Unix timestamp. EndTimestamp int64 `json:"end_timestamp" api:"required"` // The number of nodes available during this time period - Quantity int64 `json:"quantity" api:"required"` + Quantity int64 `json:"quantity" api:"required" format:"u-int64"` // Unix timestamp. StartTimestamp int64 `json:"start_timestamp" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. From 2f9af5c786a1d9af7dda595f5e5dd72a9edf723b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:25:54 +0000 Subject: [PATCH 19/38] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 0c741d5..1bb6bf9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company%2Fsfc-nodes-d786973209f42e6ca35f318f4d5bf01e4abd77205e210409c6a3fb371a99a4c5.yml -openapi_spec_hash: 03857ab189ed9fcd889e7b3fe1cc2f2f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company%2Fsfc-nodes-e799cec0ecece44c16a6a7befb5198904770f91b175632ebc38140758561790a.yml +openapi_spec_hash: aad98a06f22a81ce4c8dd72a0b949ec2 config_hash: a187153315a646ecf95709ee4a223df5 From 1aad79aad4eb6f35fc6a72077c7f4f4ac2299ba4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 03:51:04 +0000 Subject: [PATCH 20/38] chore(internal): more robust bootstrap script --- scripts/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index 5ab3066..46547f1 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response From e8e7fff59aeb8bcd5b44703b55c59665e47dde46 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:49:43 +0000 Subject: [PATCH 21/38] feat(go): add default http client with timeout --- client.go | 2 +- default_http_client.go | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 default_http_client.go diff --git a/client.go b/client.go index ee0c56f..2cc1108 100644 --- a/client.go +++ b/client.go @@ -31,7 +31,7 @@ type Client struct { // DefaultClientOptions read from the environment (SFC_NODES_BEARER_TOKEN, // SFC_NODES_BASE_URL). This should be used to initialize new clients. func DefaultClientOptions() []option.RequestOption { - defaults := []option.RequestOption{option.WithEnvironmentProduction()} + defaults := []option.RequestOption{option.WithHTTPClient(defaultHTTPClient()), option.WithEnvironmentProduction()} if o, ok := os.LookupEnv("SFC_NODES_BASE_URL"); ok { defaults = append(defaults, option.WithBaseURL(o)) } diff --git a/default_http_client.go b/default_http_client.go new file mode 100644 index 0000000..cd8945e --- /dev/null +++ b/default_http_client.go @@ -0,0 +1,24 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package sfcnodes + +import ( + "net/http" + "time" +) + +// defaultResponseHeaderTimeout bounds the time between a fully written request +// and the server's response headers. It does not apply to the response body, +// so long-running streams are unaffected. Without this, a server that accepts +// the connection but never responds would hang the request indefinitely. +const defaultResponseHeaderTimeout = 10 * time.Minute + +// defaultHTTPClient returns an [*http.Client] used when the caller does not +// supply one via [option.WithHTTPClient]. It clones [http.DefaultTransport] +// and adds a [http.Transport.ResponseHeaderTimeout] so stuck connections +// fail fast instead of compounding across retries. +func defaultHTTPClient() *http.Client { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.ResponseHeaderTimeout = defaultResponseHeaderTimeout + return &http.Client{Transport: transport} +} From c14c4ead498de87c9cc80bba147fbc6f285d64a4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:54:25 +0000 Subject: [PATCH 22/38] feat: support setting headers via env --- client.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client.go b/client.go index 2cc1108..06f788f 100644 --- a/client.go +++ b/client.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "slices" + "strings" "github.com/sfcompute/nodes-go/internal/requestconfig" "github.com/sfcompute/nodes-go/option" @@ -38,6 +39,14 @@ func DefaultClientOptions() []option.RequestOption { if o, ok := os.LookupEnv("SFC_NODES_BEARER_TOKEN"); ok { defaults = append(defaults, option.WithBearerToken(o)) } + if o, ok := os.LookupEnv("SFC_NODES_CUSTOM_HEADERS"); ok { + for _, line := range strings.Split(o, "\n") { + colon := strings.Index(line, ":") + if colon >= 0 { + defaults = append(defaults, option.WithHeader(strings.TrimSpace(line[:colon]), strings.TrimSpace(line[colon+1:]))) + } + } + } return defaults } From 9730d4fe3f18eb97108de55c10537347622c6c0a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 04:43:42 +0000 Subject: [PATCH 23/38] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 1bb6bf9..d93dc62 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company%2Fsfc-nodes-e799cec0ecece44c16a6a7befb5198904770f91b175632ebc38140758561790a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-e799cec0ecece44c16a6a7befb5198904770f91b175632ebc38140758561790a.yml openapi_spec_hash: aad98a06f22a81ce4c8dd72a0b949ec2 config_hash: a187153315a646ecf95709ee4a223df5 From bedfc12dfd17094cc8dffd0cdef24f697384470f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 04:48:17 +0000 Subject: [PATCH 24/38] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index d93dc62..0963a4d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-e799cec0ecece44c16a6a7befb5198904770f91b175632ebc38140758561790a.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-ed778ad7a0f1a043170e872139d9b342a76c4221229ef7a0ba9d0deb6eebd5da.yml openapi_spec_hash: aad98a06f22a81ce4c8dd72a0b949ec2 config_hash: a187153315a646ecf95709ee4a223df5 From 851e07b60b7b90623fb34c9d8e947677806c55ae Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 04:50:51 +0000 Subject: [PATCH 25/38] chore: avoid embedding reflect.Type for dead code elimination --- internal/apiform/encoder.go | 4 ++-- internal/apijson/decoder.go | 4 ++-- internal/apijson/encoder.go | 4 ++-- internal/apiquery/encoder.go | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index 960732e..4fb6457 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -58,7 +58,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string arrayFmt string root bool @@ -76,7 +76,7 @@ func (e *encoder) marshal(value any, writer *multipart.Writer) error { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, arrayFmt: e.arrayFmt, root: e.root, diff --git a/internal/apijson/decoder.go b/internal/apijson/decoder.go index 8c869f9..a251260 100644 --- a/internal/apijson/decoder.go +++ b/internal/apijson/decoder.go @@ -80,7 +80,7 @@ type decoderField struct { } type decoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool } @@ -108,7 +108,7 @@ func (d *decoderBuilder) unmarshalWithExactness(raw []byte, to any) (exactness, func (d *decoderBuilder) typeDecoder(t reflect.Type) decoderFunc { entry := decoderEntry{ - Type: t, + typ: t, dateFormat: d.dateFormat, root: d.root, } diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go index a601291..3ec771a 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -46,7 +46,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool } @@ -63,7 +63,7 @@ func (e *encoder) marshal(value any) ([]byte, error) { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, root: e.root, } diff --git a/internal/apiquery/encoder.go b/internal/apiquery/encoder.go index d75a89b..dd20723 100644 --- a/internal/apiquery/encoder.go +++ b/internal/apiquery/encoder.go @@ -29,7 +29,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool settings QuerySettings @@ -42,7 +42,7 @@ type Pair struct { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, root: e.root, settings: e.settings, From ce03df6b12d63a0711fd15049397c4e6f69c4071 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 04:57:44 +0000 Subject: [PATCH 26/38] chore: redact api-key headers in debug logs --- option/middleware.go | 46 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/option/middleware.go b/option/middleware.go index 8ec9dd6..4be0987 100644 --- a/option/middleware.go +++ b/option/middleware.go @@ -8,6 +8,10 @@ import ( "net/http/httputil" ) +// sensitiveLogHeaders are redacted before request and response content is +// written to the debug logger. +var sensitiveLogHeaders = []string{"authorization", "api-key", "x-api-key", "cookie", "set-cookie"} + // WithDebugLog logs the HTTP request and response content. // If the logger parameter is nil, it uses the default logger. // @@ -20,7 +24,7 @@ func WithDebugLog(logger *log.Logger) RequestOption { logger = log.Default() } - if reqBytes, err := httputil.DumpRequest(req, true); err == nil { + if reqBytes, err := dumpRedactedRequest(req); err == nil { logger.Printf("Request Content:\n%s\n", reqBytes) } @@ -29,10 +33,48 @@ func WithDebugLog(logger *log.Logger) RequestOption { return resp, err } - if respBytes, err := httputil.DumpResponse(resp, true); err == nil { + if respBytes, err := dumpRedactedResponse(resp); err == nil { logger.Printf("Response Content:\n%s\n", respBytes) } return resp, err }) } + +// dumpRedactedRequest dumps req with sensitive headers replaced. The +// original headers are restored via defer so a panic in DumpRequest cannot +// leak the placeholder map into the live request sent downstream. +func dumpRedactedRequest(req *http.Request) ([]byte, error) { + origHeaders := req.Header + req.Header = redactDebugHeaders(origHeaders) + defer func() { req.Header = origHeaders }() + return httputil.DumpRequest(req, true) +} + +func dumpRedactedResponse(resp *http.Response) ([]byte, error) { + origHeaders := resp.Header + resp.Header = redactDebugHeaders(origHeaders) + defer func() { resp.Header = origHeaders }() + return httputil.DumpResponse(resp, true) +} + +func redactDebugHeaders(headers http.Header) http.Header { + var redacted http.Header + for _, name := range sensitiveLogHeaders { + values := headers.Values(name) + if len(values) == 0 { + continue + } + if redacted == nil { + redacted = headers.Clone() + } + redacted.Del(name) + for range values { + redacted.Add(name, "***") + } + } + if redacted == nil { + return headers + } + return redacted +} From 57cc619fd65be59bbf0d85a3f25a3a9c1a07843a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 04:58:41 +0000 Subject: [PATCH 27/38] fix(go): avoid panic when http.DefaultTransport is wrapped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit defaultHTTPClient performed an unchecked type assertion on http.DefaultTransport, which panicked for any caller that had wrapped the global transport (e.g. otelhttp.NewTransport for distributed tracing). When the assertion fails, fall back to the wrapped RoundTripper as-is — preserving the caller's wrapping at the cost of ResponseHeaderTimeout, which is strictly better than panicking. --- default_http_client.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/default_http_client.go b/default_http_client.go index cd8945e..c63cde4 100644 --- a/default_http_client.go +++ b/default_http_client.go @@ -14,11 +14,17 @@ import ( const defaultResponseHeaderTimeout = 10 * time.Minute // defaultHTTPClient returns an [*http.Client] used when the caller does not -// supply one via [option.WithHTTPClient]. It clones [http.DefaultTransport] -// and adds a [http.Transport.ResponseHeaderTimeout] so stuck connections -// fail fast instead of compounding across retries. +// supply one via [option.WithHTTPClient]. When [http.DefaultTransport] is the +// stdlib [*http.Transport], it is cloned and a [http.Transport.ResponseHeaderTimeout] +// is set so stuck connections fail fast instead of compounding across retries. +// If [http.DefaultTransport] has been wrapped (for example by otelhttp for +// distributed tracing), the wrapping is preserved and the header timeout is +// skipped. func defaultHTTPClient() *http.Client { - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.ResponseHeaderTimeout = defaultResponseHeaderTimeout - return &http.Client{Transport: transport} + if t, ok := http.DefaultTransport.(*http.Transport); ok { + t = t.Clone() + t.ResponseHeaderTimeout = defaultResponseHeaderTimeout + return &http.Client{Transport: t} + } + return &http.Client{Transport: http.DefaultTransport} } From 44eaa76f451d5488b5d9921d30bc92c0d5302d56 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 03:01:28 +0000 Subject: [PATCH 28/38] ci: pin GitHub Actions to commit SHAs Pin all GitHub Actions referenced in generated workflows (both first-party `actions/*` and third-party) to immutable commit SHAs. Updating pinned actions is now a deliberate codegen-side bump rather than implicit on every workflow run. --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d36933f..f35fa9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,14 +26,14 @@ jobs: github.repository == 'stainless-sdks/sfc-nodes-go' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Get GitHub OIDC Token if: |- github.repository == 'stainless-sdks/sfc-nodes-go' && !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -53,10 +53,10 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: ./go.mod @@ -68,10 +68,10 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/sfc-nodes-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: ./go.mod From 77a27ee3287988b67c53a7a08111b91a5e08d2de Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 04:05:04 +0000 Subject: [PATCH 29/38] feat(client): optimize json encoder for internal types --- internal/encoding/json/encode.go | 21 ++-- internal/encoding/json/indent.go | 17 ++- internal/encoding/json/opt.go | 24 +++++ internal/encoding/json/stream.go | 53 +++++----- internal/encoding/json/time.go | 2 +- packages/param/encoder.go | 4 +- packages/param/encoder_test.go | 176 +++++++++++++++++++++++++++++++ 7 files changed, 260 insertions(+), 37 deletions(-) create mode 100644 internal/encoding/json/opt.go diff --git a/internal/encoding/json/encode.go b/internal/encoding/json/encode.go index 2f2564f..63a73d9 100644 --- a/internal/encoding/json/encode.go +++ b/internal/encoding/json/encode.go @@ -173,15 +173,21 @@ import ( // JSON cannot represent cyclic data structures and Marshal does not // handle them. Passing cyclic structures to Marshal will result in // an error. -func Marshal(v any) ([]byte, error) { +// EDIT(begin): add optimization options +func Marshal(v any, opts ...Option) ([]byte, error) { + // EDIT(end): add optimization options e := newEncodeState() defer encodeStatePool.Put(e) - // SHIM(begin): don't escape HTML by default - err := e.marshal(v, encOpts{escapeHTML: shims.EscapeHTMLByDefault}) + // EDIT(begin): don't escape HTML by default, and apply options + encOpts := encOpts{escapeHTML: shims.EscapeHTMLByDefault} + if opts != nil { + encOpts = encOpts.apply(opts...) + } + err := e.marshal(v, encOpts) // ORIGINAL: // err := e.marshal(v, encOpts{escapeHTML: true}) - // SHIM(end) + // EDIT(end) if err != nil { return nil, err } @@ -352,6 +358,9 @@ type encOpts struct { // EDIT(begin): save the timefmt timefmt string // EDIT(end) + // EDIT(begin): add optimization to skip compaction + skipCompaction bool + // EDIT(end) } type encoderFunc func(e *encodeState, v reflect.Value, opts encOpts) @@ -483,7 +492,7 @@ func marshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) { if err == nil { e.Grow(len(b)) out := e.AvailableBuffer() - out, err = appendCompact(out, b, opts.escapeHTML) + out, err = appendCompact(out, b, opts) e.Buffer.Write(out) } if err != nil { @@ -509,7 +518,7 @@ func addrMarshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) { if err == nil { e.Grow(len(b)) out := e.AvailableBuffer() - out, err = appendCompact(out, b, opts.escapeHTML) + out, err = appendCompact(out, b, opts) e.Buffer.Write(out) } if err != nil { diff --git a/internal/encoding/json/indent.go b/internal/encoding/json/indent.go index 01bfdf6..c9d6ca5 100644 --- a/internal/encoding/json/indent.go +++ b/internal/encoding/json/indent.go @@ -4,7 +4,9 @@ package json -import "bytes" +import ( + "bytes" +) // HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029 // characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029 @@ -41,12 +43,21 @@ func appendHTMLEscape(dst, src []byte) []byte { func Compact(dst *bytes.Buffer, src []byte) error { dst.Grow(len(src)) b := dst.AvailableBuffer() - b, err := appendCompact(b, src, false) + b, err := appendCompact(b, src, encOpts{}) dst.Write(b) return err } -func appendCompact(dst, src []byte, escape bool) ([]byte, error) { +func appendCompact(dst, src []byte, opts encOpts) ([]byte, error) { + // EDIT(begin): optimize for skipCompaction + if opts.skipCompaction { + dst = append(dst, src...) + return dst, nil + } + + escape := opts.escapeHTML + // EDIT(end) + origLen := len(dst) scan := newScanner() defer freeScanner(scan) diff --git a/internal/encoding/json/opt.go b/internal/encoding/json/opt.go new file mode 100644 index 0000000..fd6f8d2 --- /dev/null +++ b/internal/encoding/json/opt.go @@ -0,0 +1,24 @@ +// EDIT(begin): add custom options for JSON encoding +package json + +type Option func(*encOpts) + +// Every time a sub-type of [json.Marshaler] is encountered, +// skip a redundant and costly compaction step, trust it to self-compact. +// +// This is a divergence from the standard library behavior, and is only guaranteed +// safe with SDK types. +func WithSkipCompaction(b bool) Option { + return func(eos *encOpts) { + eos.skipCompaction = true + } +} + +func (eos encOpts) apply(opts ...Option) encOpts { + for _, opt := range opts { + opt(&eos) + } + return eos +} + +// EDIT(end) diff --git a/internal/encoding/json/stream.go b/internal/encoding/json/stream.go index e2d9470..652522c 100644 --- a/internal/encoding/json/stream.go +++ b/internal/encoding/json/stream.go @@ -6,7 +6,6 @@ package json import ( "bytes" - "errors" "io" ) @@ -253,30 +252,34 @@ func (enc *Encoder) SetEscapeHTML(on bool) { enc.escapeHTML = on } -// RawMessage is a raw encoded JSON value. -// It implements [Marshaler] and [Unmarshaler] and can -// be used to delay JSON decoding or precompute a JSON encoding. -type RawMessage []byte - -// MarshalJSON returns m as the JSON encoding of m. -func (m RawMessage) MarshalJSON() ([]byte, error) { - if m == nil { - return []byte("null"), nil - } - return m, nil -} - -// UnmarshalJSON sets *m to a copy of data. -func (m *RawMessage) UnmarshalJSON(data []byte) error { - if m == nil { - return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") - } - *m = append((*m)[0:0], data...) - return nil -} - -var _ Marshaler = (*RawMessage)(nil) -var _ Unmarshaler = (*RawMessage)(nil) +// EDIT(begin): remove RawMessage +// +// // RawMessage is a raw encoded JSON value. +// // It implements [Marshaler] and [Unmarshaler] and can +// // be used to delay JSON decoding or precompute a JSON encoding. +// type RawMessage []byte +// +// // MarshalJSON returns m as the JSON encoding of m. +// func (m RawMessage) MarshalJSON() ([]byte, error) { +// if m == nil { +// return []byte("null"), nil +// } +// return m, nil +// } +// +// // UnmarshalJSON sets *m to a copy of data. +// func (m *RawMessage) UnmarshalJSON(data []byte) error { +// if m == nil { +// return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") +// } +// *m = append((*m)[0:0], data...) +// return nil +// } +// +// var _ Marshaler = (*RawMessage)(nil) +// var _ Unmarshaler = (*RawMessage)(nil) +// +// EDIT(end) // A Token holds a value of one of these types: // diff --git a/internal/encoding/json/time.go b/internal/encoding/json/time.go index 9e3bda3..d2314be 100644 --- a/internal/encoding/json/time.go +++ b/internal/encoding/json/time.go @@ -50,7 +50,7 @@ func timeMarshalEncoder(e *encodeState, v reflect.Value, opts encOpts) bool { if b != nil { e.Grow(len(b)) out := e.AvailableBuffer() - out, _ = appendCompact(out, b, opts.escapeHTML) + out, _ = appendCompact(out, b, opts) e.Buffer.Write(out) return true } diff --git a/packages/param/encoder.go b/packages/param/encoder.go index 01fe75f..f5fd216 100644 --- a/packages/param/encoder.go +++ b/packages/param/encoder.go @@ -66,7 +66,7 @@ func MarshalWithExtras[T ParamStruct, R any](f T, underlying any, extras map[str } else if ovr, ok := f.Overrides(); ok { return shimjson.Marshal(ovr) } else { - return shimjson.Marshal(underlying) + return shimjson.Marshal(underlying, shimjson.WithSkipCompaction(true)) } } @@ -96,7 +96,7 @@ func MarshalUnion[T ParamStruct](metadata T, variants ...any) ([]byte, error) { Err: fmt.Errorf("expected union to have only one present variant, got %d", nPresent), } } - return shimjson.Marshal(variants[presentIdx]) + return shimjson.Marshal(variants[presentIdx], shimjson.WithSkipCompaction(true)) } // typeFor is shimmed from Go 1.23 "reflect" package diff --git a/packages/param/encoder_test.go b/packages/param/encoder_test.go index e72e18d..95beb29 100644 --- a/packages/param/encoder_test.go +++ b/packages/param/encoder_test.go @@ -1,10 +1,13 @@ package param_test import ( + "bytes" "encoding/json" + "reflect" "testing" "time" + shimjson "github.com/sfcompute/nodes-go/internal/encoding/json" "github.com/sfcompute/nodes-go/packages/param" ) @@ -375,3 +378,176 @@ func TestNullStructUnion(t *testing.T) { t.Fatalf("expected null, received %s", string(b)) } } + +// +// Compaction optimization +// + +type NonCompactedDoubleParent struct { + Prop string `json:"prop"` + Parent NonCompactedParent `json:"parent"` + + param.APIObject +} + +type NonCompactedParent struct { + BadChild NonCompacted `json:"bad_child"` + + param.APIObject +} + +type NonCompacted struct { + Raw string + + param.APIObject +} + +func (a NonCompactedDoubleParent) MarshalJSON() ([]byte, error) { + type shadow NonCompactedDoubleParent + return param.MarshalObject(a, (*shadow)(&a)) +} + +func (a NonCompactedParent) MarshalJSON() ([]byte, error) { + type shadow NonCompactedParent + return param.MarshalObject(a, (*shadow)(&a)) +} + +func (a NonCompacted) MarshalJSON() ([]byte, error) { + if a.Raw == "" { + a.Raw = nonCompactedRaw + } + return []byte(a.Raw), nil +} + +var nonCompactedRaw string = ` { "foo": "bar" } ` + +func TestAppendCompactBroken(t *testing.T) { + tests := map[string]struct { + value json.Marshaler + }{ + "red/illegal-json": { + NonCompacted{Raw: `{ "broken": "json" `}, + }, + "red/nested-with-illegal-json": { + NonCompactedParent{BadChild: NonCompacted{ + Raw: `{ "broken": "json" `, + }}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + v, err := json.Marshal(test.value) + if err == nil { + t.Fatal("expected error got", v) + } + }) + } +} + +// TestAppendCompact validates an optimization for internal SDK types to +// avoid O(keys^2) iteration over each JSON object. +// +// It's possible to intentionally trigger this behavior as both a user and +// SDK developer. However, the edge case is quite pathological and requires +// calling [json.Marshaler.MarshalJSON] rather than [json.Marshal]. +func TestAppendCompact(t *testing.T) { + + tests := map[string]struct { + value json.Marshaler + expected string + }{ + // + // Non-compacted cases + // + // Note this is how to exploit the compacter to fail, you must call [json.Marshaler.MarshalJSON] rather than [json.Marshal]. + // The type must also embed [param.APIObject] and return non-compacted JSON. + // + + "no-compact/fails-compaction": { + NonCompacted{Raw: nonCompactedRaw}, + nonCompactedRaw, + }, + "no-compact/nested-with-bad-child": { + NonCompactedParent{BadChild: NonCompacted{ + Raw: nonCompactedRaw, + }}, + `{"bad_child":` + nonCompactedRaw + `}`, + }, + "no-compact/double-nested-with-bad-child": { + NonCompactedDoubleParent{Prop: "1", Parent: NonCompactedParent{BadChild: NonCompacted{ + Raw: nonCompactedRaw, + }}}, + `{"prop":"1","parent":{"bad_child":` + nonCompactedRaw + `}}`, + }, + + // + // Compacted cases + // + + "override/spaces-within": { + param.Override[NonCompactedDoubleParent](json.RawMessage(`{"com": "pact"}`)), + `{"com":"pact"}`, + }, + "override/spaces-after": { + param.Override[NonCompactedDoubleParent](json.RawMessage(`{"com":"pact"} `)), + `{"com":"pact"}`, + }, + "override/spaces-before": { + param.Override[NonCompactedDoubleParent](json.RawMessage(` {"com":"pact"}`)), + `{"com":"pact"}`, + }, + "override/spaces-around": { + param.Override[NonCompactedDoubleParent](json.RawMessage(` { "com": "pact" }`)), + `{"com":"pact"}`, + }, + "override/override-with-nested": { + param.Override[NonCompactedDoubleParent](NonCompactedParent{}), + `{"bad_child":{"foo":"bar"}}`, + }, + "override/override-with-non-compacted": { + param.Override[NonCompactedDoubleParent](NonCompacted{}), + `{"foo":"bar"}`, + }, + } + + for name, test := range tests { + t.Run(name+"/marshal-json", func(t *testing.T) { + b, err := test.value.MarshalJSON() + if err != nil { + t.Fatalf("didn't expect error %v, expected %s", err, test.expected) + } + if string(b) != test.expected { + t.Fatalf("expected %s (%s), received %s", test.expected, reflect.TypeOf(test.value), string(b)) + } + }) + + t.Run(name+"/json-marshal", func(t *testing.T) { + b, err := json.Marshal(test.value) + if err != nil { + t.Fatalf("didn't expect error %v, expected %s", err, test.expected) + } + + // expected output of JSON Marshal should always be compacted + var compactedExpected bytes.Buffer + err = json.Compact(&compactedExpected, []byte(test.expected)) + if err != nil { + t.Fatalf("didn't expect error %v, expected %s", err, test.expected) + } + + if string(b) != compactedExpected.String() { + t.Fatalf("expected %s (%s), received %s", test.expected, reflect.TypeOf(test.value), string(b)) + } + }) + + t.Run(name+"/shimjson-marshal", func(t *testing.T) { + b, err := shimjson.Marshal(test.value) + if err != nil { + t.Fatalf("didn't expect error %v, expected %s", err, test.expected) + } + if string(b) != test.expected { + t.Logf("expected %s (%s), received %s", test.expected, reflect.TypeOf(test.value), string(b)) + } + }) + } +} From 0ae17e1a7c4894100cf2bc6c3a8098eba4e17e35 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 06:27:35 +0000 Subject: [PATCH 30/38] feat(api): api update --- .stats.yml | 6 +-- api.md | 10 ---- node.go | 5 +- node_test.go | 5 +- vm.go | 1 - vmimage.go | 134 ------------------------------------------------ vmimage_test.go | 60 ---------------------- 7 files changed, 10 insertions(+), 211 deletions(-) delete mode 100644 vmimage_test.go diff --git a/.stats.yml b/.stats.yml index 0963a4d..64b1b09 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-ed778ad7a0f1a043170e872139d9b342a76c4221229ef7a0ba9d0deb6eebd5da.yml -openapi_spec_hash: aad98a06f22a81ce4c8dd72a0b949ec2 +configured_endpoints: 13 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-d77bec75d31462e0bc321888fb26cfc72dfdaad79951224c5409c9941b4b9dea.yml +openapi_spec_hash: 63fda7c20845400a48f45dc944a6e57b config_hash: a187153315a646ecf95709ee4a223df5 diff --git a/api.md b/api.md index 1348708..3831c50 100644 --- a/api.md +++ b/api.md @@ -29,16 +29,6 @@ Methods: ## Images -Response Types: - -- sfcnodes.VMImageListResponse -- sfcnodes.VMImageGetResponse - -Methods: - -- client.VMs.Images.List(ctx context.Context) (\*sfcnodes.VMImageListResponse, error) -- client.VMs.Images.Get(ctx context.Context, imageID string) (\*sfcnodes.VMImageGetResponse, error) - # Nodes Params Types: diff --git a/node.go b/node.go index 9b5d5fa..e729a6b 100644 --- a/node.go +++ b/node.go @@ -140,6 +140,9 @@ type CreateNodesRequestParam struct { // User script to be executed during the VM's boot process Data should be base64 // encoded CloudInitUserData param.Opt[string] `json:"cloud_init_user_data,omitzero" format:"byte"` + // **Experimental — subject to change or removal without notice.** Enables + // InfiniBand. Requires hardware in the chosen zone that supports InfiniBand. + EnableInfiniband param.Opt[bool] `json:"enable_infiniband,omitzero"` // (Optional) If set, enables forwarding to the VM on port 443. Forward443 param.Opt[bool] `json:"forward_443,omitzero"` // Custom image ID to use for the VM instances @@ -151,7 +154,7 @@ type CreateNodesRequestParam struct { // false. Zone param.Opt[string] `json:"zone,omitzero"` // Custom node names Names cannot begin with 'vm*' or 'n*' as this is reserved for - // system-generated IDs Names cannot be numeric strings Names cannot exceed 128 + // system-generated IDs Names cannot be numeric strings Names cannot exceed 256 // characters Names []string `json:"names,omitzero"` // Any of "autoreserved", "reserved". diff --git a/node_test.go b/node_test.go index 85cf86f..406013b 100644 --- a/node_test.go +++ b/node_test.go @@ -32,9 +32,10 @@ func TestNodeNewWithOptionalParams(t *testing.T) { MaxPricePerNodeHour: 1600, AnyZone: sfcnodes.Bool(false), CloudInitUserData: sfcnodes.String("aGVsbG8gd29ybGQ="), + EnableInfiniband: sfcnodes.Bool(false), EndAt: sfcnodes.Int(0), Forward443: sfcnodes.Bool(false), - ImageID: sfcnodes.String("vmi_1234567890abcdef"), + ImageID: sfcnodes.String("image_1234567890abcdef"), Names: []string{"cuda-crunch"}, NodeType: sfcnodes.NodeTypeAutoreserved, StartAt: sfcnodes.Int(1640995200), @@ -173,7 +174,7 @@ func TestNodeRedeployWithOptionalParams(t *testing.T) { "id", sfcnodes.NodeRedeployParams{ CloudInitUserData: sfcnodes.String("aGVsbG8gd29ybGQ="), - ImageID: sfcnodes.String("vmi_1234567890abcdef"), + ImageID: sfcnodes.String("image_1234567890abcdef"), OverrideEmpty: sfcnodes.Bool(true), }, ) diff --git a/vm.go b/vm.go index 53d3ff9..2024391 100644 --- a/vm.go +++ b/vm.go @@ -28,7 +28,6 @@ type VMService struct { Options []option.RequestOption // Manage your Virtual Machines. Script VMScriptService - // Manage your Virtual Machines. Images VMImageService } diff --git a/vmimage.go b/vmimage.go index 25966b4..5ed9a92 100644 --- a/vmimage.go +++ b/vmimage.go @@ -3,20 +3,9 @@ package sfcnodes import ( - "context" - "errors" - "fmt" - "net/http" - "slices" - - "github.com/sfcompute/nodes-go/internal/apijson" - "github.com/sfcompute/nodes-go/internal/requestconfig" "github.com/sfcompute/nodes-go/option" - "github.com/sfcompute/nodes-go/packages/respjson" ) -// Manage your Virtual Machines. -// // VMImageService contains methods and other services that help with interacting // with the sfc-nodes API. // @@ -35,126 +24,3 @@ func NewVMImageService(opts ...option.RequestOption) (r VMImageService) { r.Options = opts return } - -// List all VM Images for the authenticated account -func (r *VMImageService) List(ctx context.Context, opts ...option.RequestOption) (res *VMImageListResponse, err error) { - opts = slices.Concat(r.Options, opts) - path := "v1/vms/images" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return res, err -} - -// Get the download URL for a VM image by ID -func (r *VMImageService) Get(ctx context.Context, imageID string, opts ...option.RequestOption) (res *VMImageGetResponse, err error) { - opts = slices.Concat(r.Options, opts) - if imageID == "" { - err = errors.New("missing required image_id parameter") - return nil, err - } - path := fmt.Sprintf("v1/vms/images/%s", imageID) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return res, err -} - -// Response body for listing images -type VMImageListResponse struct { - Data []VMImageListResponseData `json:"data" api:"required"` - HasMore bool `json:"has_more" api:"required"` - // Any of "list". - Object VMImageListResponseObject `json:"object" api:"required"` - // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. - JSON struct { - Data respjson.Field - HasMore respjson.Field - Object respjson.Field - ExtraFields map[string]respjson.Field - raw string - } `json:"-"` -} - -// Returns the unmodified JSON received from the API -func (r VMImageListResponse) RawJSON() string { return r.JSON.raw } -func (r *VMImageListResponse) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) -} - -// Response body for individual image info (used in lists) -type VMImageListResponseData struct { - // Creation timestamp as Unix timestamp in seconds - CreatedAt int64 `json:"created_at" api:"required"` - // The image ID - ImageID string `json:"image_id" api:"required"` - // Client given name of the image. Must be unique per account. - Name string `json:"name" api:"required"` - // Any of "image". - Object string `json:"object" api:"required"` - // Upload status of the image - UploadStatus string `json:"upload_status" api:"required"` - // SHA256 hash of the image file for integrity verification - Sha256Hash string `json:"sha256_hash" api:"nullable"` - // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. - JSON struct { - CreatedAt respjson.Field - ImageID respjson.Field - Name respjson.Field - Object respjson.Field - UploadStatus respjson.Field - Sha256Hash respjson.Field - ExtraFields map[string]respjson.Field - raw string - } `json:"-"` -} - -// Returns the unmodified JSON received from the API -func (r VMImageListResponseData) RawJSON() string { return r.JSON.raw } -func (r *VMImageListResponseData) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) -} - -type VMImageListResponseObject string - -const ( - VMImageListResponseObjectList VMImageListResponseObject = "list" -) - -// Response body for image download presigned URL generation -type VMImageGetResponse struct { - // The presigned URL that can be used to download the image - DownloadURL string `json:"download_url" api:"required"` - // Timestamp when the presigned URL expires (RFC 3339 format) - ExpiresAt string `json:"expires_at" api:"required"` - // The image ID - ImageID string `json:"image_id" api:"required"` - // Human readable name of the image. Must be unique per account. - Name string `json:"name" api:"required"` - // Any of "image". - Object VMImageGetResponseObject `json:"object" api:"required"` - // Size of the image file in bytes - ObjectSize int64 `json:"object_size" api:"required" format:"u-int64"` - // SHA256 hash of the image file for integrity verification - Sha256Hash string `json:"sha256_hash" api:"required"` - // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. - JSON struct { - DownloadURL respjson.Field - ExpiresAt respjson.Field - ImageID respjson.Field - Name respjson.Field - Object respjson.Field - ObjectSize respjson.Field - Sha256Hash respjson.Field - ExtraFields map[string]respjson.Field - raw string - } `json:"-"` -} - -// Returns the unmodified JSON received from the API -func (r VMImageGetResponse) RawJSON() string { return r.JSON.raw } -func (r *VMImageGetResponse) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) -} - -type VMImageGetResponseObject string - -const ( - VMImageGetResponseObjectImage VMImageGetResponseObject = "image" -) diff --git a/vmimage_test.go b/vmimage_test.go deleted file mode 100644 index 85daf31..0000000 --- a/vmimage_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -package sfcnodes_test - -import ( - "context" - "errors" - "os" - "testing" - - "github.com/sfcompute/nodes-go" - "github.com/sfcompute/nodes-go/internal/testutil" - "github.com/sfcompute/nodes-go/option" -) - -func TestVMImageList(t *testing.T) { - t.Skip("Mock server tests are disabled") - baseURL := "http://localhost:4010" - if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { - baseURL = envURL - } - if !testutil.CheckTestServer(t, baseURL) { - return - } - client := sfcnodes.NewClient( - option.WithBaseURL(baseURL), - option.WithBearerToken("My Bearer Token"), - ) - _, err := client.VMs.Images.List(context.TODO()) - if err != nil { - var apierr *sfcnodes.Error - if errors.As(err, &apierr) { - t.Log(string(apierr.DumpRequest(true))) - } - t.Fatalf("err should be nil: %s", err.Error()) - } -} - -func TestVMImageGet(t *testing.T) { - t.Skip("Mock server tests are disabled") - baseURL := "http://localhost:4010" - if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { - baseURL = envURL - } - if !testutil.CheckTestServer(t, baseURL) { - return - } - client := sfcnodes.NewClient( - option.WithBaseURL(baseURL), - option.WithBearerToken("My Bearer Token"), - ) - _, err := client.VMs.Images.Get(context.TODO(), "image_id") - if err != nil { - var apierr *sfcnodes.Error - if errors.As(err, &apierr) { - t.Log(string(apierr.DumpRequest(true))) - } - t.Fatalf("err should be nil: %s", err.Error()) - } -} From eb93c6dcb89945517c8d3e506d80bec1531a8fbd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 16:27:37 +0000 Subject: [PATCH 31/38] feat(api): api update --- .stats.yml | 4 ++-- node.go | 6 +++--- node_test.go | 24 ++++++++++++------------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.stats.yml b/.stats.yml index 64b1b09..45452e5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 13 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-d77bec75d31462e0bc321888fb26cfc72dfdaad79951224c5409c9941b4b9dea.yml -openapi_spec_hash: 63fda7c20845400a48f45dc944a6e57b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-8cf6773644c62ac11c64a98531370e4e0b06e7e7a281c89792038a840bfb3a97.yml +openapi_spec_hash: 7bdbc104d46766f5a610eec054a52ca9 config_hash: a187153315a646ecf95709ee4a223df5 diff --git a/node.go b/node.go index e729a6b..e36e4b3 100644 --- a/node.go +++ b/node.go @@ -135,14 +135,14 @@ type CreateNodesRequestParam struct { // End time as Unix timestamp in seconds If provided, end time must be aligned to // the hour If not provided, the node will be created as an autoreserved node EndAt param.Opt[int64] `json:"end_at,omitzero"` + // **Experimental — subject to change or removal without notice.** Enables + // InfiniBand. Requires hardware in the chosen zone that supports InfiniBand. + PreviewEnableInfiniband param.Opt[bool] `json:"_preview_enable_infiniband,omitzero"` // Allow auto reserved nodes to be created in any zone that meets the requirements AnyZone param.Opt[bool] `json:"any_zone,omitzero"` // User script to be executed during the VM's boot process Data should be base64 // encoded CloudInitUserData param.Opt[string] `json:"cloud_init_user_data,omitzero" format:"byte"` - // **Experimental — subject to change or removal without notice.** Enables - // InfiniBand. Requires hardware in the chosen zone that supports InfiniBand. - EnableInfiniband param.Opt[bool] `json:"enable_infiniband,omitzero"` // (Optional) If set, enables forwarding to the VM on port 443. Forward443 param.Opt[bool] `json:"forward_443,omitzero"` // Custom image ID to use for the VM instances diff --git a/node_test.go b/node_test.go index 406013b..09177dc 100644 --- a/node_test.go +++ b/node_test.go @@ -28,18 +28,18 @@ func TestNodeNewWithOptionalParams(t *testing.T) { ) _, err := client.Nodes.New(context.TODO(), sfcnodes.NodeNewParams{ CreateNodesRequest: sfcnodes.CreateNodesRequestParam{ - DesiredCount: 1, - MaxPricePerNodeHour: 1600, - AnyZone: sfcnodes.Bool(false), - CloudInitUserData: sfcnodes.String("aGVsbG8gd29ybGQ="), - EnableInfiniband: sfcnodes.Bool(false), - EndAt: sfcnodes.Int(0), - Forward443: sfcnodes.Bool(false), - ImageID: sfcnodes.String("image_1234567890abcdef"), - Names: []string{"cuda-crunch"}, - NodeType: sfcnodes.NodeTypeAutoreserved, - StartAt: sfcnodes.Int(1640995200), - Zone: sfcnodes.String("hayesvalley"), + DesiredCount: 1, + MaxPricePerNodeHour: 1600, + PreviewEnableInfiniband: sfcnodes.Bool(false), + AnyZone: sfcnodes.Bool(false), + CloudInitUserData: sfcnodes.String("aGVsbG8gd29ybGQ="), + EndAt: sfcnodes.Int(0), + Forward443: sfcnodes.Bool(false), + ImageID: sfcnodes.String("image_1234567890abcdef"), + Names: []string{"cuda-crunch"}, + NodeType: sfcnodes.NodeTypeAutoreserved, + StartAt: sfcnodes.Int(1640995200), + Zone: sfcnodes.String("hayesvalley"), }, }) if err != nil { From 57ca4f43ee107408a619cdfcc63e08203b300f3d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 04:27:33 +0000 Subject: [PATCH 32/38] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 45452e5..aba4ed0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 13 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-8cf6773644c62ac11c64a98531370e4e0b06e7e7a281c89792038a840bfb3a97.yml -openapi_spec_hash: 7bdbc104d46766f5a610eec054a52ca9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-92c25ef5a0eefcd88287edd4de0589c948908f558a03a0aa15ffa961a1415845.yml +openapi_spec_hash: e5d9664ddfbca394030b9a9e24246dc7 config_hash: a187153315a646ecf95709ee4a223df5 From bca26e229a3a6120f77df90f693989b54d26a7b1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 06:24:16 +0000 Subject: [PATCH 33/38] feat(api): update config to account for breaking changes --- .stats.yml | 6 +- api.md | 10 +++ vm.go | 1 + vmimage.go | 187 ++++++++++++++++++++++++++++++++++++++++++++++++ vmimage_test.go | 66 +++++++++++++++++ 5 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 vmimage_test.go diff --git a/.stats.yml b/.stats.yml index aba4ed0..acf0f25 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 13 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-92c25ef5a0eefcd88287edd4de0589c948908f558a03a0aa15ffa961a1415845.yml +configured_endpoints: 15 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-2c9b78374ba932f9d32014dc900b5ddf337fbb5653f60e19bad443be3b80bc7d.yml openapi_spec_hash: e5d9664ddfbca394030b9a9e24246dc7 -config_hash: a187153315a646ecf95709ee4a223df5 +config_hash: 88b3601402d13c8448777f36b5cafb9b diff --git a/api.md b/api.md index 3831c50..07d564e 100644 --- a/api.md +++ b/api.md @@ -29,6 +29,16 @@ Methods: ## Images +Response Types: + +- sfcnodes.VMImageListResponse +- sfcnodes.VMImageGetResponse + +Methods: + +- client.VMs.Images.List(ctx context.Context, query sfcnodes.VMImageListParams) (\*sfcnodes.VMImageListResponse, error) +- client.VMs.Images.Get(ctx context.Context, id string) (\*sfcnodes.VMImageGetResponse, error) + # Nodes Params Types: diff --git a/vm.go b/vm.go index 2024391..7da2430 100644 --- a/vm.go +++ b/vm.go @@ -28,6 +28,7 @@ type VMService struct { Options []option.RequestOption // Manage your Virtual Machines. Script VMScriptService + // Custom machine images for instances. Images VMImageService } diff --git a/vmimage.go b/vmimage.go index 5ed9a92..335b5f4 100644 --- a/vmimage.go +++ b/vmimage.go @@ -3,9 +3,23 @@ package sfcnodes import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "slices" + + "github.com/sfcompute/nodes-go/internal/apijson" + "github.com/sfcompute/nodes-go/internal/apiquery" + "github.com/sfcompute/nodes-go/internal/requestconfig" "github.com/sfcompute/nodes-go/option" + "github.com/sfcompute/nodes-go/packages/param" + "github.com/sfcompute/nodes-go/packages/respjson" ) +// Custom machine images for instances. +// // VMImageService contains methods and other services that help with interacting // with the sfc-nodes API. // @@ -24,3 +38,176 @@ func NewVMImageService(opts ...option.RequestOption) (r VMImageService) { r.Options = opts return } + +// > ⚠️ This endpoint is in [preview](/preview/about-preview). +// +// List images in the specified workspace. Pass `sfc:workspace:sfcompute:public` as +// the workspace to list sfc-provided public images instead. +func (r *VMImageService) List(ctx context.Context, query VMImageListParams, opts ...option.RequestOption) (res *VMImageListResponse, err error) { + opts = slices.Concat(r.Options, opts) + opts = append([]option.RequestOption{option.WithBaseURL("https://api.sfcompute.com/")}, opts...) + path := "preview/v2/images" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return res, err +} + +// > ⚠️ This endpoint is in [preview](/preview/about-preview). +// +// Retrieve an image by ID. Returns both user-owned and public images. +func (r *VMImageService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *VMImageGetResponse, err error) { + opts = slices.Concat(r.Options, opts) + opts = append([]option.RequestOption{option.WithBaseURL("https://api.sfcompute.com/")}, opts...) + if id == "" { + err = errors.New("missing required id parameter") + return nil, err + } + path := fmt.Sprintf("preview/v2/images/%s", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return res, err +} + +type VMImageListResponse struct { + Data []VMImageListResponseData `json:"data" api:"required"` + HasMore bool `json:"has_more" api:"required"` + // Any of "list". + Object VMImageListResponseObject `json:"object" api:"required"` + Cursor string `json:"cursor" api:"nullable"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Data respjson.Field + HasMore respjson.Field + Object respjson.Field + Cursor respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r VMImageListResponse) RawJSON() string { return r.JSON.raw } +func (r *VMImageListResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type VMImageListResponseData struct { + ID string `json:"id" api:"required"` + // Unix timestamp. + CreatedAt int64 `json:"created_at" api:"required"` + Name string `json:"name" api:"required"` + // Any of "image". + Object string `json:"object" api:"required"` + Owner string `json:"owner" api:"required"` + // A resource path for a image resource. Format: + // sfc:image:::. + ResourcePath string `json:"resource_path" api:"required"` + // Any of "started", "uploading", "completed", "failed", "revoked". + UploadStatus string `json:"upload_status" api:"required"` + Workspace string `json:"workspace" api:"required"` + Provider string `json:"provider" api:"nullable"` + Sha256 string `json:"sha256" api:"nullable"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + CreatedAt respjson.Field + Name respjson.Field + Object respjson.Field + Owner respjson.Field + ResourcePath respjson.Field + UploadStatus respjson.Field + Workspace respjson.Field + Provider respjson.Field + Sha256 respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r VMImageListResponseData) RawJSON() string { return r.JSON.raw } +func (r *VMImageListResponseData) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type VMImageListResponseObject string + +const ( + VMImageListResponseObjectList VMImageListResponseObject = "list" +) + +type VMImageGetResponse struct { + ID string `json:"id" api:"required"` + // Unix timestamp. + CreatedAt int64 `json:"created_at" api:"required"` + Name string `json:"name" api:"required"` + // Any of "image". + Object VMImageGetResponseObject `json:"object" api:"required"` + Owner string `json:"owner" api:"required"` + // A resource path for a image resource. Format: + // sfc:image:::. + ResourcePath string `json:"resource_path" api:"required"` + // Any of "started", "uploading", "completed", "failed", "revoked". + UploadStatus VMImageGetResponseUploadStatus `json:"upload_status" api:"required"` + Workspace string `json:"workspace" api:"required"` + Provider string `json:"provider" api:"nullable"` + Sha256 string `json:"sha256" api:"nullable"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + CreatedAt respjson.Field + Name respjson.Field + Object respjson.Field + Owner respjson.Field + ResourcePath respjson.Field + UploadStatus respjson.Field + Workspace respjson.Field + Provider respjson.Field + Sha256 respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r VMImageGetResponse) RawJSON() string { return r.JSON.raw } +func (r *VMImageGetResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type VMImageGetResponseObject string + +const ( + VMImageGetResponseObjectImage VMImageGetResponseObject = "image" +) + +type VMImageGetResponseUploadStatus string + +const ( + VMImageGetResponseUploadStatusStarted VMImageGetResponseUploadStatus = "started" + VMImageGetResponseUploadStatusUploading VMImageGetResponseUploadStatus = "uploading" + VMImageGetResponseUploadStatusCompleted VMImageGetResponseUploadStatus = "completed" + VMImageGetResponseUploadStatusFailed VMImageGetResponseUploadStatus = "failed" + VMImageGetResponseUploadStatusRevoked VMImageGetResponseUploadStatus = "revoked" +) + +type VMImageListParams struct { + // Filter by workspace. Pass `sfc:workspace:sfcompute:public` to list sfc-provided + // public images. + Workspace string `query:"workspace" api:"required" json:"-"` + // Cursor for backward pagination. + EndingBefore param.Opt[string] `query:"ending_before,omitzero" json:"-"` + // Maximum number of results to return (1-200, default 50). + Limit param.Opt[int64] `query:"limit,omitzero" format:"u-int32" json:"-"` + // Cursor for forward pagination (from a previous response's `cursor` field). + StartingAfter param.Opt[string] `query:"starting_after,omitzero" json:"-"` + // Filter by image ID (repeatable). + ID []string `query:"id,omitzero" json:"-"` + paramObj +} + +// URLQuery serializes [VMImageListParams]'s query parameters as `url.Values`. +func (r VMImageListParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatRepeat, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/vmimage_test.go b/vmimage_test.go new file mode 100644 index 0000000..51b862b --- /dev/null +++ b/vmimage_test.go @@ -0,0 +1,66 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package sfcnodes_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/sfcompute/nodes-go" + "github.com/sfcompute/nodes-go/internal/testutil" + "github.com/sfcompute/nodes-go/option" +) + +func TestVMImageListWithOptionalParams(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := sfcnodes.NewClient( + option.WithBaseURL(baseURL), + option.WithBearerToken("My Bearer Token"), + ) + _, err := client.VMs.Images.List(context.TODO(), sfcnodes.VMImageListParams{ + Workspace: "wksp_k3R-nX9vLm7Qp2Yw5Jd8F", + ID: []string{"image_k3R-nX9vLm7Qp2Yw5Jd8F"}, + EndingBefore: sfcnodes.String("imagec_gqXR7s0Kj5mHvE2wNpLc4Q"), + Limit: sfcnodes.Int(1), + StartingAfter: sfcnodes.String("imagec_gqXR7s0Kj5mHvE2wNpLc4Q"), + }) + if err != nil { + var apierr *sfcnodes.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestVMImageGet(t *testing.T) { + t.Skip("Mock server tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := sfcnodes.NewClient( + option.WithBaseURL(baseURL), + option.WithBearerToken("My Bearer Token"), + ) + _, err := client.VMs.Images.Get(context.TODO(), "image_k3R-nX9vLm7Qp2Yw5Jd8F") + if err != nil { + var apierr *sfcnodes.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} From 991672696161ce58200f397f8915bc4b51301c34 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 06:26:18 +0000 Subject: [PATCH 34/38] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index acf0f25..af54321 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-2c9b78374ba932f9d32014dc900b5ddf337fbb5653f60e19bad443be3b80bc7d.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-bef77b99228a46dabf4fa0d83d626af67a1f16adf612b90810b99591cb435dcd.yml openapi_spec_hash: e5d9664ddfbca394030b9a9e24246dc7 -config_hash: 88b3601402d13c8448777f36b5cafb9b +config_hash: 4ea510d6a5c84c7055b51a248a2aacbf From 351062650a2701426f5bd4a744de3b9c1c41c398 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 06:40:47 +0000 Subject: [PATCH 35/38] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index af54321..5fce0e9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-bef77b99228a46dabf4fa0d83d626af67a1f16adf612b90810b99591cb435dcd.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-d7dece4e944ac11225e837381a5f771dc82aa2feb37cc5ddcb83c3ed09367771.yml openapi_spec_hash: e5d9664ddfbca394030b9a9e24246dc7 -config_hash: 4ea510d6a5c84c7055b51a248a2aacbf +config_hash: 8457a42ab599fb499cdacdb3ff40cfe9 From 85940976fa7a1fad13d0ee62e4325f7b17a2bfb2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 03:39:32 +0000 Subject: [PATCH 36/38] feat(api): api update --- .stats.yml | 4 ++-- vmimage.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5fce0e9..1523999 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-d7dece4e944ac11225e837381a5f771dc82aa2feb37cc5ddcb83c3ed09367771.yml -openapi_spec_hash: e5d9664ddfbca394030b9a9e24246dc7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-679b42a61deffd275fd9e01d6e2c0024540cd0059f4cc01bb52cc2876aaff3af.yml +openapi_spec_hash: 8c3eea1499910eb3683d774fe1a21f79 config_hash: 8457a42ab599fb499cdacdb3ff40cfe9 diff --git a/vmimage.go b/vmimage.go index 335b5f4..5864a22 100644 --- a/vmimage.go +++ b/vmimage.go @@ -39,7 +39,7 @@ func NewVMImageService(opts ...option.RequestOption) (r VMImageService) { return } -// > ⚠️ This endpoint is in [preview](/preview/about-preview). +// > ⚠️ This endpoint is in [public preview](/preview/roadmap). // // List images in the specified workspace. Pass `sfc:workspace:sfcompute:public` as // the workspace to list sfc-provided public images instead. @@ -51,7 +51,7 @@ func (r *VMImageService) List(ctx context.Context, query VMImageListParams, opts return res, err } -// > ⚠️ This endpoint is in [preview](/preview/about-preview). +// > ⚠️ This endpoint is in [public preview](/preview/roadmap). // // Retrieve an image by ID. Returns both user-owned and public images. func (r *VMImageService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *VMImageGetResponse, err error) { From 5c530522281441ba93e17b182dcde27e30fb43cc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 02:27:33 +0000 Subject: [PATCH 37/38] feat(api): api update --- .stats.yml | 4 ++-- shared/constant/constants.go | 9 +++++++++ vmimage.go | 36 +++++++++++------------------------- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1523999..a937ca5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-679b42a61deffd275fd9e01d6e2c0024540cd0059f4cc01bb52cc2876aaff3af.yml -openapi_spec_hash: 8c3eea1499910eb3683d774fe1a21f79 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company/sfc-nodes-ac16f44d89080c75848b7db501921b159820ec69b23cac750448c429df12abdb.yml +openapi_spec_hash: e58f2d7597976dca2450667819526e74 config_hash: 8457a42ab599fb499cdacdb3ff40cfe9 diff --git a/shared/constant/constants.go b/shared/constant/constants.go index 423ee03..fbb1136 100644 --- a/shared/constant/constants.go +++ b/shared/constant/constants.go @@ -18,6 +18,15 @@ func ValueOf[T Constant[T]]() T { return t.Default() } +type Image string // Always "image" +type List string // Always "list" + +func (c Image) Default() Image { return "image" } +func (c List) Default() List { return "list" } + +func (c Image) MarshalJSON() ([]byte, error) { return marshalString(c) } +func (c List) MarshalJSON() ([]byte, error) { return marshalString(c) } + type constant[T any] interface { Constant[T] *T diff --git a/vmimage.go b/vmimage.go index 5864a22..5be98e2 100644 --- a/vmimage.go +++ b/vmimage.go @@ -16,6 +16,7 @@ import ( "github.com/sfcompute/nodes-go/option" "github.com/sfcompute/nodes-go/packages/param" "github.com/sfcompute/nodes-go/packages/respjson" + "github.com/sfcompute/nodes-go/shared/constant" ) // Custom machine images for instances. @@ -69,9 +70,8 @@ func (r *VMImageService) Get(ctx context.Context, id string, opts ...option.Requ type VMImageListResponse struct { Data []VMImageListResponseData `json:"data" api:"required"` HasMore bool `json:"has_more" api:"required"` - // Any of "list". - Object VMImageListResponseObject `json:"object" api:"required"` - Cursor string `json:"cursor" api:"nullable"` + Object constant.List `json:"object" default:"list"` + Cursor string `json:"cursor" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { Data respjson.Field @@ -92,11 +92,10 @@ func (r *VMImageListResponse) UnmarshalJSON(data []byte) error { type VMImageListResponseData struct { ID string `json:"id" api:"required"` // Unix timestamp. - CreatedAt int64 `json:"created_at" api:"required"` - Name string `json:"name" api:"required"` - // Any of "image". - Object string `json:"object" api:"required"` - Owner string `json:"owner" api:"required"` + CreatedAt int64 `json:"created_at" api:"required"` + Name string `json:"name" api:"required"` + Object constant.Image `json:"object" default:"image"` + Owner string `json:"owner" api:"required"` // A resource path for a image resource. Format: // sfc:image:::. ResourcePath string `json:"resource_path" api:"required"` @@ -128,20 +127,13 @@ func (r *VMImageListResponseData) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -type VMImageListResponseObject string - -const ( - VMImageListResponseObjectList VMImageListResponseObject = "list" -) - type VMImageGetResponse struct { ID string `json:"id" api:"required"` // Unix timestamp. - CreatedAt int64 `json:"created_at" api:"required"` - Name string `json:"name" api:"required"` - // Any of "image". - Object VMImageGetResponseObject `json:"object" api:"required"` - Owner string `json:"owner" api:"required"` + CreatedAt int64 `json:"created_at" api:"required"` + Name string `json:"name" api:"required"` + Object constant.Image `json:"object" default:"image"` + Owner string `json:"owner" api:"required"` // A resource path for a image resource. Format: // sfc:image:::. ResourcePath string `json:"resource_path" api:"required"` @@ -173,12 +165,6 @@ func (r *VMImageGetResponse) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -type VMImageGetResponseObject string - -const ( - VMImageGetResponseObjectImage VMImageGetResponseObject = "image" -) - type VMImageGetResponseUploadStatus string const ( From dcaca0e7924196b7f6d4de6ba423837cc7178ef1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 02:28:07 +0000 Subject: [PATCH 38/38] release: 0.1.0-alpha.6 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++ README.md | 2 +- internal/version.go | 2 +- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e8285b7..4f9005e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.5" + ".": "0.1.0-alpha.6" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5187da7..3a366a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,50 @@ # Changelog +## 0.1.0-alpha.6 (2026-05-21) + +Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/sfcompute/nodes-go/compare/v0.1.0-alpha.5...v0.1.0-alpha.6) + +### Features + +* **api:** api update ([5c53052](https://github.com/sfcompute/nodes-go/commit/5c530522281441ba93e17b182dcde27e30fb43cc)) +* **api:** api update ([8594097](https://github.com/sfcompute/nodes-go/commit/85940976fa7a1fad13d0ee62e4325f7b17a2bfb2)) +* **api:** api update ([eb93c6d](https://github.com/sfcompute/nodes-go/commit/eb93c6dcb89945517c8d3e506d80bec1531a8fbd)) +* **api:** api update ([0ae17e1](https://github.com/sfcompute/nodes-go/commit/0ae17e1a7c4894100cf2bc6c3a8098eba4e17e35)) +* **api:** update config to account for breaking changes ([bca26e2](https://github.com/sfcompute/nodes-go/commit/bca26e229a3a6120f77df90f693989b54d26a7b1)) +* **client:** optimize json encoder for internal types ([77a27ee](https://github.com/sfcompute/nodes-go/commit/77a27ee3287988b67c53a7a08111b91a5e08d2de)) +* **go:** add default http client with timeout ([e8e7fff](https://github.com/sfcompute/nodes-go/commit/e8e7fff59aeb8bcd5b44703b55c59665e47dde46)) +* **internal:** support comma format in multipart form encoding ([ecfc0ce](https://github.com/sfcompute/nodes-go/commit/ecfc0ceb5a993acb87fc73731ba19c0e24349ea9)) +* support setting headers via env ([c14c4ea](https://github.com/sfcompute/nodes-go/commit/c14c4ead498de87c9cc80bba147fbc6f285d64a4)) + + +### Bug Fixes + +* better respect format tags from the spec ([0b4c0ec](https://github.com/sfcompute/nodes-go/commit/0b4c0ecae518042d6ceecb8525718fb141e3e1c8)) +* fix issue with unmarshaling in some cases ([fa94f5f](https://github.com/sfcompute/nodes-go/commit/fa94f5f06356b76992a572891c762c3fe3559dc0)) +* **go:** avoid panic when http.DefaultTransport is wrapped ([57cc619](https://github.com/sfcompute/nodes-go/commit/57cc619fd65be59bbf0d85a3f25a3a9c1a07843a)) +* prevent duplicate ? in query params ([179abd8](https://github.com/sfcompute/nodes-go/commit/179abd873e3776a78b5f465b2878981f684c0001)) + + +### Chores + +* avoid embedding reflect.Type for dead code elimination ([851e07b](https://github.com/sfcompute/nodes-go/commit/851e07b60b7b90623fb34c9d8e947677806c55ae)) +* **ci:** skip lint on metadata-only changes ([59072fa](https://github.com/sfcompute/nodes-go/commit/59072fa7b6eda168b36e5702c27e85cd99ab84cc)) +* **ci:** skip uploading artifacts on stainless-internal branches ([d5f51e8](https://github.com/sfcompute/nodes-go/commit/d5f51e8e5330f29e7c05a58fea234a90d3c28a86)) +* **ci:** support opting out of skipping builds on metadata-only commits ([046b6e1](https://github.com/sfcompute/nodes-go/commit/046b6e1a7cb29da1c78c90944644f50e5629f304)) +* **client:** fix multipart serialisation of Default() fields ([68d8f8e](https://github.com/sfcompute/nodes-go/commit/68d8f8e87b235485f65c079a52cd3c381d9936be)) +* **internal:** codegen related update ([0efeaca](https://github.com/sfcompute/nodes-go/commit/0efeacad5ecdbea83c326a17bb7a2f593afdcdcb)) +* **internal:** codegen related update ([6905ec5](https://github.com/sfcompute/nodes-go/commit/6905ec5c3e4361acdbb1eda50ebcb869330c0504)) +* **internal:** minor cleanup ([d4332e4](https://github.com/sfcompute/nodes-go/commit/d4332e4aeae3e5a569fc586ab430182cb705de17)) +* **internal:** more robust bootstrap script ([1aad79a](https://github.com/sfcompute/nodes-go/commit/1aad79aad4eb6f35fc6a72077c7f4f4ac2299ba4)) +* **internal:** support default value struct tag ([9be56ed](https://github.com/sfcompute/nodes-go/commit/9be56edd00c8e78ee7d3cd56604eeebe63df376b)) +* **internal:** tweak CI branches ([f1ab274](https://github.com/sfcompute/nodes-go/commit/f1ab27477b86808c8942b6ce899803ac150c8b16)) +* **internal:** update gitignore ([c997311](https://github.com/sfcompute/nodes-go/commit/c9973115ba8dcfb4eb10f7cab9c438c7729779fb)) +* **internal:** use explicit returns ([f740ed8](https://github.com/sfcompute/nodes-go/commit/f740ed8beb5a3e52d0c879e31a9ed47f85af233d)) +* **internal:** use explicit returns in more places ([f8f3570](https://github.com/sfcompute/nodes-go/commit/f8f3570dcb58aaecc9139b4bf4e93c1a77f4a711)) +* redact api-key headers in debug logs ([ce03df6](https://github.com/sfcompute/nodes-go/commit/ce03df6b12d63a0711fd15049397c4e6f69c4071)) +* remove unnecessary error check for url parsing ([9a31c22](https://github.com/sfcompute/nodes-go/commit/9a31c22ba410290e150a66b0f7bec72c9ae131be)) +* update docs for api:"required" ([afd6c27](https://github.com/sfcompute/nodes-go/commit/afd6c27ac6b3ae51794c4098293b9f1a676cf2a2)) + ## 0.1.0-alpha.5 (2026-02-25) Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/sfcompute/nodes-go/compare/v0.1.0-alpha.4...v0.1.0-alpha.5) diff --git a/README.md b/README.md index e13453c..88044a7 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Or to pin the version: ```sh -go get -u 'github.com/sfcompute/nodes-go@v0.1.0-alpha.5' +go get -u 'github.com/sfcompute/nodes-go@v0.1.0-alpha.6' ``` diff --git a/internal/version.go b/internal/version.go index 2aad167..0ff00c6 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.1.0-alpha.5" // x-release-please-version +const PackageVersion = "0.1.0-alpha.6" // x-release-please-version