From af678e8c794307a6bd47476acff3ca42a7a52546 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 30 Jan 2026 20:23:18 +0000
Subject: [PATCH 1/2] feat: Use resources module for input validation
---
.stats.yml | 8 +++----
api.md | 1 +
resource.go | 5 +++++
volume.go | 59 +++++++++++++++++++++++++++++++++++++++++++++-----
volume_test.go | 33 ++++++++++++++++++++++++++++
5 files changed, 97 insertions(+), 9 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 5e112da..54f52d3 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 36
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-fbe706932aedef16a8bb04fe361b1e8659ae074ff9122f4e9e28914087a677b8.yml
-openapi_spec_hash: 6ba4858b44cb317676c58f33bfa1426a
-config_hash: 542144891263a3626eabcb2fd4f4f3e5
+configured_endpoints: 37
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-cf9805637aaf9bf590b5ebc1e7430414850aff0c8ad727a017df290ea8df9913.yml
+openapi_spec_hash: e27144cf0b24dc74fbdb77d8e24d818f
+config_hash: d67a314ba1fda8242041000fc9160e92
diff --git a/api.md b/api.md
index 4255a7d..c73bfa1 100644
--- a/api.md
+++ b/api.md
@@ -65,6 +65,7 @@ Methods:
- client.Volumes.New(ctx context.Context, body hypeman.VolumeNewParams) (\*hypeman.Volume, error)
- client.Volumes.List(ctx context.Context) (\*[]hypeman.Volume, error)
- client.Volumes.Delete(ctx context.Context, id string) error
+- client.Volumes.NewFromArchive(ctx context.Context, body io.Reader, params hypeman.VolumeNewFromArchiveParams) (\*hypeman.Volume, error)
- client.Volumes.Get(ctx context.Context, id string) (\*hypeman.Volume, error)
# Devices
diff --git a/resource.go b/resource.go
index 7806701..ef6dd47 100644
--- a/resource.go
+++ b/resource.go
@@ -158,6 +158,8 @@ type ResourceAllocation struct {
CPU int64 `json:"cpu"`
// Disk allocated in bytes (overlay + volumes)
DiskBytes int64 `json:"disk_bytes"`
+ // Disk I/O bandwidth limit in bytes/sec
+ DiskIoBps int64 `json:"disk_io_bps"`
// Instance identifier
InstanceID string `json:"instance_id"`
// Instance name
@@ -172,6 +174,7 @@ type ResourceAllocation struct {
JSON struct {
CPU respjson.Field
DiskBytes respjson.Field
+ DiskIoBps respjson.Field
InstanceID respjson.Field
InstanceName respjson.Field
MemoryBytes respjson.Field
@@ -230,6 +233,7 @@ type Resources struct {
Memory ResourceStatus `json:"memory,required"`
Network ResourceStatus `json:"network,required"`
DiskBreakdown DiskBreakdown `json:"disk_breakdown"`
+ DiskIo ResourceStatus `json:"disk_io"`
// GPU resource status. Null if no GPUs available.
GPU GPUResourceStatus `json:"gpu,nullable"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
@@ -240,6 +244,7 @@ type Resources struct {
Memory respjson.Field
Network respjson.Field
DiskBreakdown respjson.Field
+ DiskIo respjson.Field
GPU respjson.Field
ExtraFields map[string]respjson.Field
raw string
diff --git a/volume.go b/volume.go
index 75992b3..1951b76 100644
--- a/volume.go
+++ b/volume.go
@@ -3,14 +3,20 @@
package hypeman
import (
+ "bytes"
"context"
"errors"
"fmt"
+ "io"
+ "mime/multipart"
"net/http"
+ "net/url"
"slices"
"time"
+ "github.com/kernel/hypeman-go/internal/apiform"
"github.com/kernel/hypeman-go/internal/apijson"
+ "github.com/kernel/hypeman-go/internal/apiquery"
"github.com/kernel/hypeman-go/internal/requestconfig"
"github.com/kernel/hypeman-go/option"
"github.com/kernel/hypeman-go/packages/param"
@@ -36,11 +42,7 @@ func NewVolumeService(opts ...option.RequestOption) (r VolumeService) {
return
}
-// Creates a new volume. Supports two modes:
-//
-// - JSON body: Creates an empty volume of the specified size
-// - Multipart form: Creates a volume pre-populated with content from a tar.gz
-// archive
+// Creates a new empty volume of the specified size.
func (r *VolumeService) New(ctx context.Context, body VolumeNewParams, opts ...option.RequestOption) (res *Volume, err error) {
opts = slices.Concat(r.Options, opts)
path := "volumes"
@@ -69,6 +71,16 @@ func (r *VolumeService) Delete(ctx context.Context, id string, opts ...option.Re
return
}
+// Creates a new volume pre-populated with content from a tar.gz archive. The
+// archive is streamed directly into the volume's root directory.
+func (r *VolumeService) NewFromArchive(ctx context.Context, body io.Reader, params VolumeNewFromArchiveParams, opts ...option.RequestOption) (res *Volume, err error) {
+ opts = slices.Concat(r.Options, opts)
+ opts = append([]option.RequestOption{option.WithRequestBody("application/gzip", body)}, opts...)
+ path := "volumes/from-archive"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
+ return
+}
+
// Get volume details
func (r *VolumeService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Volume, err error) {
opts = slices.Concat(r.Options, opts)
@@ -150,3 +162,40 @@ func (r VolumeNewParams) MarshalJSON() (data []byte, err error) {
func (r *VolumeNewParams) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
+
+type VolumeNewFromArchiveParams struct {
+ // Volume name
+ Name string `query:"name,required" json:"-"`
+ // Maximum size in GB (extraction fails if content exceeds this)
+ SizeGB int64 `query:"size_gb,required" json:"-"`
+ // Optional custom volume ID (auto-generated if not provided)
+ ID param.Opt[string] `query:"id,omitzero" json:"-"`
+ paramObj
+}
+
+func (r VolumeNewFromArchiveParams) MarshalMultipart() (data []byte, contentType string, err error) {
+ buf := bytes.NewBuffer(nil)
+ writer := multipart.NewWriter(buf)
+ err = apiform.MarshalRoot(r, writer)
+ if err == nil {
+ err = apiform.WriteExtras(writer, r.ExtraFields())
+ }
+ if err != nil {
+ writer.Close()
+ return nil, "", err
+ }
+ err = writer.Close()
+ if err != nil {
+ return nil, "", err
+ }
+ return buf.Bytes(), writer.FormDataContentType(), nil
+}
+
+// URLQuery serializes [VolumeNewFromArchiveParams]'s query parameters as
+// `url.Values`.
+func (r VolumeNewFromArchiveParams) URLQuery() (v url.Values, err error) {
+ return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
+ ArrayFormat: apiquery.ArrayQueryFormatComma,
+ NestedFormat: apiquery.NestedQueryFormatBrackets,
+ })
+}
diff --git a/volume_test.go b/volume_test.go
index 09e1759..05dbc71 100644
--- a/volume_test.go
+++ b/volume_test.go
@@ -3,8 +3,10 @@
package hypeman_test
import (
+ "bytes"
"context"
"errors"
+ "io"
"os"
"testing"
@@ -86,6 +88,37 @@ func TestVolumeDelete(t *testing.T) {
}
}
+func TestVolumeNewFromArchiveWithOptionalParams(t *testing.T) {
+ t.Skip("Prism 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.Volumes.NewFromArchive(
+ context.TODO(),
+ io.Reader(bytes.NewBuffer([]byte("some file contents"))),
+ hypeman.VolumeNewFromArchiveParams{
+ Name: "name",
+ SizeGB: 0,
+ ID: hypeman.String("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 TestVolumeGet(t *testing.T) {
t.Skip("Prism tests are disabled")
baseURL := "http://localhost:4010"
From 30c85af355239d1a23d4d960c1f5c1bc2f2f50ea Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 30 Jan 2026 20:23:33 +0000
Subject: [PATCH 2/2] release: 0.9.6
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 13 +++++++++++++
README.md | 2 +-
internal/version.go | 2 +-
4 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 6d78745..8e9551f 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.9.0"
+ ".": "0.9.6"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4405152..c46e448 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# Changelog
+## 0.9.6 (2026-01-30)
+
+Full Changelog: [v0.9.0...v0.9.6](https://github.com/kernel/hypeman-go/compare/v0.9.0...v0.9.6)
+
+### Features
+
+* add boot time optimizations for faster VM startup ([3992761](https://github.com/kernel/hypeman-go/commit/3992761e3ad8ebb0cc22fb7408199b068e9d8013))
+* Add to stainless config new API endpoints ([de008e8](https://github.com/kernel/hypeman-go/commit/de008e89fadbaedde6554181618fb03c71b49465))
+* **api:** manual updates ([f60e600](https://github.com/kernel/hypeman-go/commit/f60e60015bb9ce18c7083963d9ecd11c980de495))
+* **builds:** implement two-tier build cache with per-repo token scopes ([0e29d03](https://github.com/kernel/hypeman-go/commit/0e29d03d94cf50a0d0e83c323f7ed9f2e15f3e61))
+* **client:** add a convenient param.SetJSON helper ([7fea166](https://github.com/kernel/hypeman-go/commit/7fea1660f3d17d8a35f5d2f6aa352b553785624b))
+* Use resources module for input validation ([af678e8](https://github.com/kernel/hypeman-go/commit/af678e8c794307a6bd47476acff3ca42a7a52546))
+
## 0.9.0 (2026-01-05)
Full Changelog: [v0.8.0...v0.9.0](https://github.com/kernel/hypeman-go/compare/v0.8.0...v0.9.0)
diff --git a/README.md b/README.md
index 0b76851..4a876db 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ Or to pin the version:
```sh
-go get -u 'github.com/kernel/hypeman-go@v0.8.0'
+go get -u 'github.com/kernel/hypeman-go@v0.9.6'
```
diff --git a/internal/version.go b/internal/version.go
index 0e818c5..d7771e2 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -2,4 +2,4 @@
package internal
-const PackageVersion = "0.9.0" // x-release-please-version
+const PackageVersion = "0.9.6" // x-release-please-version