diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5eb5472..156d569 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,8 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/kernel-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: |- github.repository == 'stainless-sdks/kernel-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 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/ diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cc51f6f..fc0d7ff 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.44.0" + ".": "0.45.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index be60802..8a5c9d0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 104 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-bb2ac8e0d3a1c08e8afcbcbad7cb733d0f84bd22a8d233c1ec3100a01ee078ae.yml -openapi_spec_hash: a83f7d1c422c85d6dc6158af7afe1d09 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-aeb5ea5c2632fe7fd905d509bc6cbb06999d17c458ec44ffd713935ba5b848f9.yml +openapi_spec_hash: fef45a8569f1d3de04c86e95b1112665 config_hash: 16e4457a0bb26e98a335a1c2a572290a diff --git a/CHANGELOG.md b/CHANGELOG.md index 7785f78..057b444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.45.0 (2026-03-26) + +Full Changelog: [v0.44.0...v0.45.0](https://github.com/kernel/kernel-go-sdk/compare/v0.44.0...v0.45.0) + +### Features + +* [kernel-1008] browser pools add custom policy ([e740db7](https://github.com/kernel/kernel-go-sdk/commit/e740db79ec07414b5b0b5ff3be98b9e089a9bb45)) + + +### Chores + +* **ci:** skip lint on metadata-only changes ([8296238](https://github.com/kernel/kernel-go-sdk/commit/8296238ce0b45903e3d40420100aa29a3ab105fd)) +* **client:** fix multipart serialisation of Default() fields ([cabda49](https://github.com/kernel/kernel-go-sdk/commit/cabda49a25334cd3fa3fc747f69b8a515e2b77d9)) +* **internal:** support default value struct tag ([dd77af4](https://github.com/kernel/kernel-go-sdk/commit/dd77af45f77a98e96f5d140bb737377f1d72456a)) +* **internal:** update gitignore ([48baab7](https://github.com/kernel/kernel-go-sdk/commit/48baab7aa0e23ed519a08b52142927340a8d451b)) + ## 0.44.0 (2026-03-20) Full Changelog: [v0.43.0...v0.44.0](https://github.com/kernel/kernel-go-sdk/compare/v0.43.0...v0.44.0) diff --git a/README.md b/README.md index 9594193..9ff93db 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Or to pin the version: ```sh -go get -u 'github.com/kernel/kernel-go-sdk@v0.44.0' +go get -u 'github.com/kernel/kernel-go-sdk@v0.45.0' ``` diff --git a/app.go b/app.go index d3f4230..bac9291 100644 --- a/app.go +++ b/app.go @@ -76,7 +76,7 @@ type AppListResponse struct { // Environment variables configured for this app version EnvVars map[string]string `json:"env_vars" api:"required"` // Deployment region code - Region constant.AwsUsEast1a `json:"region" api:"required"` + Region constant.AwsUsEast1a `json:"region" default:"aws.us-east-1a"` // Version label for the application Version string `json:"version" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. diff --git a/authconnection.go b/authconnection.go index a9df222..ed76924 100644 --- a/authconnection.go +++ b/authconnection.go @@ -875,7 +875,7 @@ func (r *AuthConnectionFollowResponseUnion) UnmarshalJSON(data []byte) error { // An event representing the current state of a managed auth flow. type AuthConnectionFollowResponseManagedAuthState struct { // Event type identifier (always "managed_auth_state"). - Event constant.ManagedAuthState `json:"event" api:"required"` + Event constant.ManagedAuthState `json:"event" default:"managed_auth_state"` // Current flow status. // // Any of "IN_PROGRESS", "SUCCESS", "FAILED", "EXPIRED", "CANCELED". diff --git a/browserpool.go b/browserpool.go index 303f117..54ac2aa 100644 --- a/browserpool.go +++ b/browserpool.go @@ -173,6 +173,11 @@ type BrowserPoolBrowserPoolConfig struct { // your organization's pooled sessions limit (the sum of all pool sizes cannot // exceed your limit). Size int64 `json:"size" api:"required"` + // Custom Chrome enterprise policy overrides applied to all browsers in this pool. + // Keys are Chrome enterprise policy names; values must match their expected types. + // Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See + // https://chromeenterprise.google/policies/ + ChromePolicy map[string]any `json:"chrome_policy"` // List of browser extensions to load into the session. Provide each by id or name. Extensions []shared.BrowserExtension `json:"extensions"` // Percentage of the pool to fill per minute. Defaults to 10%. @@ -213,6 +218,7 @@ type BrowserPoolBrowserPoolConfig struct { // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { Size respjson.Field + ChromePolicy respjson.Field Extensions respjson.Field FillRatePerMinute respjson.Field Headless respjson.Field @@ -337,6 +343,11 @@ type BrowserPoolNewParams struct { // Default idle timeout in seconds for browsers acquired from this pool before they // are destroyed. Defaults to 600 seconds if not specified TimeoutSeconds param.Opt[int64] `json:"timeout_seconds,omitzero"` + // Custom Chrome enterprise policy overrides applied to all browsers in this pool. + // Keys are Chrome enterprise policy names; values must match their expected types. + // Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See + // https://chromeenterprise.google/policies/ + ChromePolicy map[string]any `json:"chrome_policy,omitzero"` // List of browser extensions to load into the session. Provide each by id or name. Extensions []shared.BrowserExtensionParam `json:"extensions,omitzero"` // Profile selection for the browser session. Provide either id or name. If @@ -393,6 +404,11 @@ type BrowserPoolUpdateParams struct { // Default idle timeout in seconds for browsers acquired from this pool before they // are destroyed. Defaults to 600 seconds if not specified TimeoutSeconds param.Opt[int64] `json:"timeout_seconds,omitzero"` + // Custom Chrome enterprise policy overrides applied to all browsers in this pool. + // Keys are Chrome enterprise policy names; values must match their expected types. + // Blocked: kernel-managed policies (extensions, proxy, CDP/automation). See + // https://chromeenterprise.google/policies/ + ChromePolicy map[string]any `json:"chrome_policy,omitzero"` // List of browser extensions to load into the session. Provide each by id or name. Extensions []shared.BrowserExtensionParam `json:"extensions,omitzero"` // Profile selection for the browser session. Provide either id or name. If diff --git a/browserpool_test.go b/browserpool_test.go index 0bed0f3..fd59d64 100644 --- a/browserpool_test.go +++ b/browserpool_test.go @@ -29,6 +29,9 @@ func TestBrowserPoolNewWithOptionalParams(t *testing.T) { ) _, err := client.BrowserPools.New(context.TODO(), kernel.BrowserPoolNewParams{ Size: 10, + ChromePolicy: map[string]any{ + "foo": "bar", + }, Extensions: []shared.BrowserExtensionParam{{ ID: kernel.String("id"), Name: kernel.String("name"), @@ -100,7 +103,10 @@ func TestBrowserPoolUpdateWithOptionalParams(t *testing.T) { context.TODO(), "id_or_name", kernel.BrowserPoolUpdateParams{ - Size: 10, + Size: 10, + ChromePolicy: map[string]any{ + "foo": "bar", + }, DiscardAllIdle: kernel.Bool(false), Extensions: []shared.BrowserExtensionParam{{ ID: kernel.String("id"), diff --git a/deployment.go b/deployment.go index 37b7e64..5d0380c 100644 --- a/deployment.go +++ b/deployment.go @@ -130,7 +130,7 @@ type DeploymentStateEvent struct { // Deployment record information. Deployment DeploymentStateEventDeployment `json:"deployment" api:"required"` // Event type identifier (always "deployment_state"). - Event constant.DeploymentState `json:"event" api:"required"` + Event constant.DeploymentState `json:"event" default:"deployment_state"` // Time the state was reported. Timestamp time.Time `json:"timestamp" api:"required" format:"date-time"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. @@ -156,7 +156,7 @@ type DeploymentStateEventDeployment struct { // Timestamp when the deployment was created CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"` // Deployment region code - Region constant.AwsUsEast1a `json:"region" api:"required"` + Region constant.AwsUsEast1a `json:"region" default:"aws.us-east-1a"` // Current status of the deployment // // Any of "queued", "in_progress", "running", "failed", "stopped". @@ -197,7 +197,7 @@ type DeploymentNewResponse struct { // Timestamp when the deployment was created CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"` // Deployment region code - Region constant.AwsUsEast1a `json:"region" api:"required"` + Region constant.AwsUsEast1a `json:"region" default:"aws.us-east-1a"` // Current status of the deployment // // Any of "queued", "in_progress", "running", "failed", "stopped". @@ -249,7 +249,7 @@ type DeploymentGetResponse struct { // Timestamp when the deployment was created CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"` // Deployment region code - Region constant.AwsUsEast1a `json:"region" api:"required"` + Region constant.AwsUsEast1a `json:"region" default:"aws.us-east-1a"` // Current status of the deployment // // Any of "queued", "in_progress", "running", "failed", "stopped". @@ -301,7 +301,7 @@ type DeploymentListResponse struct { // Timestamp when the deployment was created CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"` // Deployment region code - Region constant.AwsUsEast1a `json:"region" api:"required"` + Region constant.AwsUsEast1a `json:"region" default:"aws.us-east-1a"` // Current status of the deployment // // Any of "queued", "in_progress", "running", "failed", "stopped". @@ -433,9 +433,9 @@ type DeploymentFollowResponseAppVersionSummaryEvent struct { // Name of the application AppName string `json:"app_name" api:"required"` // Event type identifier (always "app_version_summary"). - Event constant.AppVersionSummary `json:"event" api:"required"` + Event constant.AppVersionSummary `json:"event" default:"app_version_summary"` // Deployment region code - Region constant.AwsUsEast1a `json:"region" api:"required"` + Region constant.AwsUsEast1a `json:"region" default:"aws.us-east-1a"` // Time the state was reported. Timestamp time.Time `json:"timestamp" api:"required" format:"date-time"` // Version label for the application diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index b93e5a3..cf9e767 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 76d8a92..0660fd1 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]" 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..afe611e 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -12,6 +12,8 @@ import ( "time" "github.com/tidwall/sjson" + + shimjson "github.com/kernel/kernel-go-sdk/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 { diff --git a/internal/version.go b/internal/version.go index 27842b2..0f8ee25 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.44.0" // x-release-please-version +const PackageVersion = "0.45.0" // x-release-please-version diff --git a/invocation.go b/invocation.go index 3a75523..775dd29 100644 --- a/invocation.go +++ b/invocation.go @@ -150,7 +150,7 @@ func (r *InvocationService) ListBrowsers(ctx context.Context, id string, opts .. // An event representing the current state of an invocation. type InvocationStateEvent struct { // Event type identifier (always "invocation_state"). - Event constant.InvocationState `json:"event" api:"required"` + Event constant.InvocationState `json:"event" default:"invocation_state"` Invocation InvocationStateEventInvocation `json:"invocation" api:"required"` // Time the state was reported. Timestamp time.Time `json:"timestamp" api:"required" format:"date-time"` diff --git a/shared/shared.go b/shared/shared.go index 10fcf8f..09471ec 100644 --- a/shared/shared.go +++ b/shared/shared.go @@ -257,7 +257,7 @@ func (r *ErrorDetail) UnmarshalJSON(data []byte) error { type ErrorEvent struct { Error ErrorModel `json:"error" api:"required"` // Event type identifier (always "error"). - Event constant.Error `json:"event" api:"required"` + Event constant.Error `json:"event" default:"error"` // Time the error occurred. Timestamp time.Time `json:"timestamp" api:"required" format:"date-time"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. @@ -307,7 +307,7 @@ func (r *ErrorModel) UnmarshalJSON(data []byte) error { // Heartbeat event sent periodically to keep SSE connection alive. type HeartbeatEvent struct { // Event type identifier (always "sse_heartbeat"). - Event constant.SseHeartbeat `json:"event" api:"required"` + Event constant.SseHeartbeat `json:"event" default:"sse_heartbeat"` // Time the heartbeat was sent. Timestamp time.Time `json:"timestamp" api:"required" format:"date-time"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. @@ -331,7 +331,7 @@ func (HeartbeatEvent) ImplAuthConnectionFollowResponseUnion() {} // A log entry from the application. type LogEvent struct { // Event type identifier (always "log"). - Event constant.Log `json:"event" api:"required"` + Event constant.Log `json:"event" default:"log"` // Log message text. Message string `json:"message" api:"required"` // Time the log entry was produced.