From b3cf1c76eced867da0eb555364e8f26e6899c0ba 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:04:14 +0000 Subject: [PATCH 1/4] 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 40a13cc..cda2e5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,14 +26,14 @@ jobs: github.repository == 'stainless-sdks/hypeman-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/hypeman-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/hypeman-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 d8bd64d9ec2cd00f3c219e1540cb18c1bc43ecb8 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:11:10 +0000 Subject: [PATCH 2/4] 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 bdd596e..7c150e1 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 581f0af..bf75cd5 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 c0a7585..00463b9 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 55afdf7..1cc1f71 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/kernel/hypeman-go/internal/encoding/json" "github.com/kernel/hypeman-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 8ea492b994dffbea6d99f1897d87aaa627835d1b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 15:59:02 +0000 Subject: [PATCH 3/4] feat: Model template as an instance state instead of a separate registry --- .stats.yml | 8 +++--- api.md | 2 ++ instance.go | 72 ++++++++++++++++++++++++++++++++++-------------- instance_test.go | 46 +++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 24 deletions(-) diff --git a/.stats.yml b/.stats.yml index 0064ed8..7a06cda 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 52 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/hypeman-0aececd4fa79c47cb7222167d6746064c53b69eb70ee14252be71ccc31e6d2a2.yml -openapi_spec_hash: c514624af74c74835e3187b857184ff2 -config_hash: ed668fae8826ff533f38df16c9664f44 +configured_endpoints: 54 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/hypeman-48b300e51c488f55e3f36024515bdb78a8fa5fe44c6fbde99d3f8a34c16df1cb.yml +openapi_spec_hash: 1b53b5b26006f74ccbbb8facc57a9d44 +config_hash: dfacce742631adb22616891760f57888 diff --git a/api.md b/api.md index 105a110..7f1a9a9 100644 --- a/api.md +++ b/api.md @@ -59,9 +59,11 @@ Methods: - client.Instances.Update(ctx context.Context, id string, body hypeman.InstanceUpdateParams) (\*hypeman.Instance, error) - client.Instances.List(ctx context.Context, query hypeman.InstanceListParams) (\*[]hypeman.Instance, error) - client.Instances.Delete(ctx context.Context, id string) error +- client.Instances.DemoteTemplate(ctx context.Context, id string) (\*hypeman.Instance, error) - client.Instances.Fork(ctx context.Context, id string, body hypeman.InstanceForkParams) (\*hypeman.Instance, error) - client.Instances.Get(ctx context.Context, id string) (\*hypeman.Instance, error) - client.Instances.Logs(ctx context.Context, id string, query hypeman.InstanceLogsParams) (\*string, error) +- client.Instances.PromoteTemplate(ctx context.Context, id string) (\*hypeman.Instance, error) - client.Instances.Restore(ctx context.Context, id string) (\*hypeman.Instance, error) - client.Instances.Standby(ctx context.Context, id string, body hypeman.InstanceStandbyParams) (\*hypeman.Instance, error) - client.Instances.Start(ctx context.Context, id string, body hypeman.InstanceStartParams) (\*hypeman.Instance, error) diff --git a/instance.go b/instance.go index 1bd5caa..f3b31b9 100644 --- a/instance.go +++ b/instance.go @@ -93,6 +93,18 @@ func (r *InstanceService) Delete(ctx context.Context, id string, opts ...option. return err } +// Demote a template back to standby so it can be restored or deleted +func (r *InstanceService) DemoteTemplate(ctx context.Context, id string, opts ...option.RequestOption) (res *Instance, err error) { + opts = slices.Concat(r.Options, opts) + if id == "" { + err = errors.New("missing required id parameter") + return nil, err + } + path := fmt.Sprintf("instances/%s/demote-template", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) + return res, err +} + // Fork an instance from stopped, standby, or running (with from_running=true) func (r *InstanceService) Fork(ctx context.Context, id string, body InstanceForkParams, opts ...option.RequestOption) (res *Instance, err error) { opts = slices.Concat(r.Options, opts) @@ -142,6 +154,18 @@ func (r *InstanceService) LogsStreaming(ctx context.Context, id string, query In return ssestream.NewStream[string](ssestream.NewDecoder(raw), err) } +// Promote a standby instance into a fork-only template +func (r *InstanceService) PromoteTemplate(ctx context.Context, id string, opts ...option.RequestOption) (res *Instance, err error) { + opts = slices.Concat(r.Options, opts) + if id == "" { + err = errors.New("missing required id parameter") + return nil, err + } + path := fmt.Sprintf("instances/%s/promote-template", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) + return res, err +} + // Restore instance from standby func (r *InstanceService) Restore(ctx context.Context, id string, opts ...option.RequestOption) (res *Instance, err error) { opts = slices.Concat(r.Options, opts) @@ -391,17 +415,19 @@ type Instance struct { Name string `json:"name" api:"required"` // Instance state: // - // - Created: VMM created but not started (Cloud Hypervisor native) - // - Initializing: VM is running while guest init is still in progress - // - Running: Guest program has started and instance is ready - // - Paused: VM is paused (Cloud Hypervisor native) - // - Shutdown: VM shut down but VMM exists (Cloud Hypervisor native) - // - Stopped: No VMM running, no snapshot exists - // - Standby: No VMM running, snapshot exists (can be restored) - // - Unknown: Failed to determine state (see state_error for details) + // - Created: VMM created but not started (Cloud Hypervisor native) + // - Initializing: VM is running while guest init is still in progress + // - Running: Guest program has started and instance is ready + // - Paused: VM is paused (Cloud Hypervisor native) + // - Shutdown: VM shut down but VMM exists (Cloud Hypervisor native) + // - Stopped: No VMM running, no snapshot exists + // - Standby: No VMM running, snapshot exists (can be restored) + // - Template: Standby snapshot promoted to a fork-only parent; cannot wake while + // forks exist + // - Unknown: Failed to determine state (see state_error for details) // // Any of "Created", "Initializing", "Running", "Paused", "Shutdown", "Stopped", - // "Standby", "Unknown". + // "Standby", "Template", "Unknown". State InstanceState `json:"state" api:"required"` // Linux-only automatic standby policy based on active inbound TCP connections // observed from the host conntrack table. @@ -496,14 +522,16 @@ func (r *Instance) UnmarshalJSON(data []byte) error { // Instance state: // -// - Created: VMM created but not started (Cloud Hypervisor native) -// - Initializing: VM is running while guest init is still in progress -// - Running: Guest program has started and instance is ready -// - Paused: VM is paused (Cloud Hypervisor native) -// - Shutdown: VM shut down but VMM exists (Cloud Hypervisor native) -// - Stopped: No VMM running, no snapshot exists -// - Standby: No VMM running, snapshot exists (can be restored) -// - Unknown: Failed to determine state (see state_error for details) +// - Created: VMM created but not started (Cloud Hypervisor native) +// - Initializing: VM is running while guest init is still in progress +// - Running: Guest program has started and instance is ready +// - Paused: VM is paused (Cloud Hypervisor native) +// - Shutdown: VM shut down but VMM exists (Cloud Hypervisor native) +// - Stopped: No VMM running, no snapshot exists +// - Standby: No VMM running, snapshot exists (can be restored) +// - Template: Standby snapshot promoted to a fork-only parent; cannot wake while +// forks exist +// - Unknown: Failed to determine state (see state_error for details) type InstanceState string const ( @@ -514,6 +542,7 @@ const ( InstanceStateShutdown InstanceState = "Shutdown" InstanceStateStopped InstanceState = "Stopped" InstanceStateStandby InstanceState = "Standby" + InstanceStateTemplate InstanceState = "Template" InstanceStateUnknown InstanceState = "Unknown" ) @@ -916,7 +945,7 @@ type WaitForStateResponse struct { // Current instance state when the wait completed // // Any of "Created", "Initializing", "Running", "Paused", "Shutdown", "Stopped", - // "Standby", "Unknown". + // "Standby", "Template", "Unknown". State WaitForStateResponseState `json:"state" api:"required"` // Whether the timeout expired before the target state was reached TimedOut bool `json:"timed_out" api:"required"` @@ -949,6 +978,7 @@ const ( WaitForStateResponseStateShutdown WaitForStateResponseState = "Shutdown" WaitForStateResponseStateStopped WaitForStateResponseState = "Stopped" WaitForStateResponseStateStandby WaitForStateResponseState = "Standby" + WaitForStateResponseStateTemplate WaitForStateResponseState = "Template" WaitForStateResponseStateUnknown WaitForStateResponseState = "Unknown" ) @@ -1213,7 +1243,7 @@ type InstanceListParams struct { // Filter instances by state (e.g., Running, Stopped) // // Any of "Created", "Initializing", "Running", "Paused", "Shutdown", "Stopped", - // "Standby", "Unknown". + // "Standby", "Template", "Unknown". State InstanceListParamsState `query:"state,omitzero" json:"-"` // Filter instances by tag key-value pairs. Uses deepObject style: // ?tags[team]=backend&tags[env]=staging Multiple entries are ANDed together. All @@ -1241,6 +1271,7 @@ const ( InstanceListParamsStateShutdown InstanceListParamsState = "Shutdown" InstanceListParamsStateStopped InstanceListParamsState = "Stopped" InstanceListParamsStateStandby InstanceListParamsState = "Standby" + InstanceListParamsStateTemplate InstanceListParamsState = "Template" InstanceListParamsStateUnknown InstanceListParamsState = "Unknown" ) @@ -1364,7 +1395,7 @@ type InstanceWaitParams struct { // Target state to wait for // // Any of "Created", "Initializing", "Running", "Paused", "Shutdown", "Stopped", - // "Standby", "Unknown". + // "Standby", "Template", "Unknown". State InstanceWaitParamsState `query:"state,omitzero" api:"required" json:"-"` // Maximum duration to wait (Go duration format, e.g. "30s", "2m"). Capped at 5 // minutes. Defaults to 60 seconds. @@ -1391,5 +1422,6 @@ const ( InstanceWaitParamsStateShutdown InstanceWaitParamsState = "Shutdown" InstanceWaitParamsStateStopped InstanceWaitParamsState = "Stopped" InstanceWaitParamsStateStandby InstanceWaitParamsState = "Standby" + InstanceWaitParamsStateTemplate InstanceWaitParamsState = "Template" InstanceWaitParamsStateUnknown InstanceWaitParamsState = "Unknown" ) diff --git a/instance_test.go b/instance_test.go index 9932e97..e9a3d20 100644 --- a/instance_test.go +++ b/instance_test.go @@ -197,6 +197,29 @@ func TestInstanceDelete(t *testing.T) { } } +func TestInstanceDemoteTemplate(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 := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Instances.DemoteTemplate(context.TODO(), "id") + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + func TestInstanceForkWithOptionalParams(t *testing.T) { t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" @@ -251,6 +274,29 @@ func TestInstanceGet(t *testing.T) { } } +func TestInstancePromoteTemplate(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 := hypeman.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Instances.PromoteTemplate(context.TODO(), "id") + if err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + func TestInstanceRestore(t *testing.T) { t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" From f5b9b5d04c78a5ccaf00bea8eed5152a6598f9d7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 16:00:37 +0000 Subject: [PATCH 4/4] release: 0.20.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 9 +++++++++ README.md | 2 +- internal/version.go | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e756293..0c2ecec 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.19.0" + ".": "0.20.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c44166..505d33d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.20.0 (2026-05-14) + +Full Changelog: [v0.19.0...v0.20.0](https://github.com/kernel/hypeman-go/compare/v0.19.0...v0.20.0) + +### Features + +* **client:** optimize json encoder for internal types ([d8bd64d](https://github.com/kernel/hypeman-go/commit/d8bd64d9ec2cd00f3c219e1540cb18c1bc43ecb8)) +* Model template as an instance state instead of a separate registry ([8ea492b](https://github.com/kernel/hypeman-go/commit/8ea492b994dffbea6d99f1897d87aaa627835d1b)) + ## 0.19.0 (2026-05-12) Full Changelog: [v0.18.0...v0.19.0](https://github.com/kernel/hypeman-go/compare/v0.18.0...v0.19.0) diff --git a/README.md b/README.md index cc2ac62..1f0803c 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Or to pin the version: ```sh -go get -u 'github.com/kernel/hypeman-go@v0.19.0' +go get -u 'github.com/kernel/hypeman-go@v0.20.0' ``` diff --git a/internal/version.go b/internal/version.go index 1117f72..6c84346 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.19.0" // x-release-please-version +const PackageVersion = "0.20.0" // x-release-please-version