From 53ef042e66ce7e050877be0e08613c41e66026de 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:58:51 +0000 Subject: [PATCH 01/10] 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 4562f586a6f26caebf5908b9e899cd578a3af4c1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 04:06:28 +0000 Subject: [PATCH 02/10] 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 6105b35..6babac2 100644 --- a/client.go +++ b/client.go @@ -31,7 +31,7 @@ type Client struct { // DefaultClientOptions read from the environment (HYPEMAN_API_KEY, // HYPEMAN_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("HYPEMAN_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..a3a5b19 --- /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 hypeman + +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 3b6d6cd3ff96bef7d0f01e58116ca74eef5c8e1c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 04:11:39 +0000 Subject: [PATCH 03/10] feat: support setting headers via env --- client.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client.go b/client.go index 6babac2..acf25cf 100644 --- a/client.go +++ b/client.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "slices" + "strings" "github.com/kernel/hypeman-go/internal/requestconfig" "github.com/kernel/hypeman-go/option" @@ -38,6 +39,14 @@ func DefaultClientOptions() []option.RequestOption { if o, ok := os.LookupEnv("HYPEMAN_API_KEY"); ok { defaults = append(defaults, option.WithAPIKey(o)) } + if o, ok := os.LookupEnv("HYPEMAN_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 607f811b04f8ffb7cd34ca5083b6a141cf0e5808 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 06:13:43 +0000 Subject: [PATCH 04/10] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index f729551..3aec88b 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%2Fhypeman-c6da5deb317c83b7a10434593eb22ec7cb27009aba0b92efaefbbe21884054ad.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/hypeman-c6da5deb317c83b7a10434593eb22ec7cb27009aba0b92efaefbbe21884054ad.yml openapi_spec_hash: ff73a0e1f7a8bd5a5d1ae38d994bb9cd config_hash: ed668fae8826ff533f38df16c9664f44 From 4f74645ce521a06a979e65718fd80cb408eacc9d 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:53:18 +0000 Subject: [PATCH 05/10] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 3aec88b..fb86afe 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-c6da5deb317c83b7a10434593eb22ec7cb27009aba0b92efaefbbe21884054ad.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/hypeman-75aa32bfceac1a349267baf08a13df9d8dc37fd07525f45675d064654eac0e1f.yml openapi_spec_hash: ff73a0e1f7a8bd5a5d1ae38d994bb9cd config_hash: ed668fae8826ff533f38df16c9664f44 From 768a2a12c13728f05946bd4c0f24e5f3a90f8620 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:56:40 +0000 Subject: [PATCH 06/10] 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 fd5efd2..edaf378 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 0225b9f..b6b9332 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 bf61641..fc38322 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 ac31823..ef4114c 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 0f703395ab4135be91ac30c47ed94bf2e61989b5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 05:21:09 +0000 Subject: [PATCH 07/10] 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 90abdb2fe804de224236e5a0556b5675afb49c09 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 05:22:00 +0000 Subject: [PATCH 08/10] 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 a3a5b19..bfda19f 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 312c26f13a0c22a91b902b51c13a431452dca79d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 12:39:51 +0000 Subject: [PATCH 09/10] feat: Track per-phase duration on each instance --- .stats.yml | 4 ++-- instance.go | 65 ++++++++++++++++++++++++++++++++--------------------- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/.stats.yml b/.stats.yml index fb86afe..0064ed8 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-75aa32bfceac1a349267baf08a13df9d8dc37fd07525f45675d064654eac0e1f.yml -openapi_spec_hash: ff73a0e1f7a8bd5a5d1ae38d994bb9cd +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel/hypeman-0aececd4fa79c47cb7222167d6746064c53b69eb70ee14252be71ccc31e6d2a2.yml +openapi_spec_hash: c514624af74c74835e3187b857184ff2 config_hash: ed668fae8826ff533f38df16c9664f44 diff --git a/instance.go b/instance.go index b93ddc6..1bd5caa 100644 --- a/instance.go +++ b/instance.go @@ -406,6 +406,10 @@ type Instance struct { // Linux-only automatic standby policy based on active inbound TCP connections // observed from the host conntrack table. AutoStandby AutoStandbyPolicy `json:"auto_standby"` + // The lifecycle phase the instance is currently in. + CurrentPhase string `json:"current_phase"` + // When the instance entered current_phase. + CurrentPhaseSince time.Time `json:"current_phase_since" format:"date-time"` // Disk I/O rate limit (human-readable, e.g., "100MB/s") DiskIoBps string `json:"disk_io_bps"` // Environment variables @@ -429,6 +433,12 @@ type Instance struct { Network InstanceNetwork `json:"network"` // Writable overlay disk size (human-readable) OverlaySize string `json:"overlay_size"` + // Cumulative milliseconds the instance has spent in each lifecycle phase, + // including time accrued in the current phase up to the response time. Keys mirror + // instance states lowercased (running, standby, paused, stopped, created, + // initializing, shutdown). Consumers (e.g. billing) sum the phases they consider + // billable. + PhaseDurationsMs map[string]int64 `json:"phase_durations_ms"` // Base memory size (human-readable) Size string `json:"size"` SnapshotPolicy SnapshotPolicy `json:"snapshot_policy"` @@ -446,32 +456,35 @@ type Instance struct { Volumes []VolumeMount `json:"volumes"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - ID respjson.Field - CreatedAt respjson.Field - Image respjson.Field - Name respjson.Field - State respjson.Field - AutoStandby respjson.Field - DiskIoBps respjson.Field - Env respjson.Field - ExitCode respjson.Field - ExitMessage respjson.Field - GPU respjson.Field - HasSnapshot respjson.Field - HotplugSize respjson.Field - Hypervisor respjson.Field - Network respjson.Field - OverlaySize respjson.Field - Size respjson.Field - SnapshotPolicy respjson.Field - StartedAt respjson.Field - StateError respjson.Field - StoppedAt respjson.Field - Tags respjson.Field - Vcpus respjson.Field - Volumes respjson.Field - ExtraFields map[string]respjson.Field - raw string + ID respjson.Field + CreatedAt respjson.Field + Image respjson.Field + Name respjson.Field + State respjson.Field + AutoStandby respjson.Field + CurrentPhase respjson.Field + CurrentPhaseSince respjson.Field + DiskIoBps respjson.Field + Env respjson.Field + ExitCode respjson.Field + ExitMessage respjson.Field + GPU respjson.Field + HasSnapshot respjson.Field + HotplugSize respjson.Field + Hypervisor respjson.Field + Network respjson.Field + OverlaySize respjson.Field + PhaseDurationsMs respjson.Field + Size respjson.Field + SnapshotPolicy respjson.Field + StartedAt respjson.Field + StateError respjson.Field + StoppedAt respjson.Field + Tags respjson.Field + Vcpus respjson.Field + Volumes respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } From 20b99292e35e6510e6d52ebb4c642af32ff7c904 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 12:40:20 +0000 Subject: [PATCH 10/10] release: 0.19.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 22 ++++++++++++++++++++++ README.md | 2 +- internal/version.go | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4ad3fef..e756293 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.18.0" + ".": "0.19.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1327172..3c44166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 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) + +### Features + +* **go:** add default http client with timeout ([4562f58](https://github.com/kernel/hypeman-go/commit/4562f586a6f26caebf5908b9e899cd578a3af4c1)) +* support setting headers via env ([3b6d6cd](https://github.com/kernel/hypeman-go/commit/3b6d6cd3ff96bef7d0f01e58116ca74eef5c8e1c)) +* Track per-phase duration on each instance ([312c26f](https://github.com/kernel/hypeman-go/commit/312c26f13a0c22a91b902b51c13a431452dca79d)) + + +### Bug Fixes + +* **go:** avoid panic when http.DefaultTransport is wrapped ([90abdb2](https://github.com/kernel/hypeman-go/commit/90abdb2fe804de224236e5a0556b5675afb49c09)) + + +### Chores + +* avoid embedding reflect.Type for dead code elimination ([768a2a1](https://github.com/kernel/hypeman-go/commit/768a2a12c13728f05946bd4c0f24e5f3a90f8620)) +* **internal:** more robust bootstrap script ([53ef042](https://github.com/kernel/hypeman-go/commit/53ef042e66ce7e050877be0e08613c41e66026de)) +* redact api-key headers in debug logs ([0f70339](https://github.com/kernel/hypeman-go/commit/0f703395ab4135be91ac30c47ed94bf2e61989b5)) + ## 0.18.0 (2026-04-17) Full Changelog: [v0.17.0...v0.18.0](https://github.com/kernel/hypeman-go/compare/v0.17.0...v0.18.0) diff --git a/README.md b/README.md index 5fd1408..cc2ac62 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.18.0' +go get -u 'github.com/kernel/hypeman-go@v0.19.0' ``` diff --git a/internal/version.go b/internal/version.go index 8dc40e7..1117f72 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.18.0" // x-release-please-version +const PackageVersion = "0.19.0" // x-release-please-version