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