diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index aa2972c..ab1f59c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,6 +13,33 @@ on:
- 'stl-preview-base/**'
jobs:
+ build:
+ timeout-minutes: 10
+ name: build
+ permissions:
+ contents: read
+ id-token: write
+ runs-on: ${{ github.repository == 'stainless-sdks/hypeman-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: |-
+ github.repository == 'stainless-sdks/hypeman-go' &&
+ (github.event_name == 'push' || github.event.pull_request.head.repo.fork)
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Get GitHub OIDC Token
+ if: github.repository == 'stainless-sdks/hypeman-go'
+ id: github-oidc
+ uses: actions/github-script@v8
+ with:
+ script: core.setOutput('github_token', await core.getIDToken());
+
+ - name: Upload tarball
+ if: github.repository == 'stainless-sdks/hypeman-go'
+ env:
+ URL: https://pkg.stainless.com/s
+ AUTH: ${{ steps.github-oidc.outputs.github_token }}
+ SHA: ${{ github.sha }}
+ run: ./scripts/utils/upload-artifact.sh
lint:
timeout-minutes: 10
name: lint
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index a26ebfc..8f3e0a4 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.14.0"
+ ".": "0.15.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index c9ace89..511176d 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 37
+configured_endpoints: 39
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-68bd472fc1704fc7ff7ed01b4213dda068a0865d42693d47ecef90651526febb.yml
openapi_spec_hash: 18ec995954b05d8dfb1e9e3254cf579a
-config_hash: d452c139da1e46a44a68b91e8a40de72
+config_hash: 368f8c9248e41f12124ab83b6f5b2eec
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33fa4c8..646e8f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# Changelog
+## 0.15.0 (2026-03-04)
+
+Full Changelog: [v0.14.0...v0.15.0](https://github.com/kernel/hypeman-go/compare/v0.14.0...v0.15.0)
+
+### Features
+
+* Add fork operation to stainless config ([5ab52b7](https://github.com/kernel/hypeman-go/commit/5ab52b7c74031eb7e874615aa42c00090cb00b51))
+
+
+### Chores
+
+* **internal:** codegen related update ([83363a5](https://github.com/kernel/hypeman-go/commit/83363a5275d88e131d0736516fea215faaab84a7))
+
## 0.14.0 (2026-03-02)
Full Changelog: [v0.13.0...v0.14.0](https://github.com/kernel/hypeman-go/compare/v0.13.0...v0.14.0)
diff --git a/README.md b/README.md
index 5543274..47f44f5 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.14.0'
+go get -u 'github.com/kernel/hypeman-go@v0.15.0'
```
diff --git a/api.md b/api.md
index dfd33e2..3fa23c1 100644
--- a/api.md
+++ b/api.md
@@ -30,6 +30,7 @@ Params Types:
Response Types:
- hypeman.Instance
+- hypeman.InstanceStats
- hypeman.PathInfo
- hypeman.VolumeMount
@@ -38,12 +39,14 @@ Methods:
- client.Instances.New(ctx context.Context, body hypeman.InstanceNewParams) (\*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.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.Restore(ctx context.Context, id string) (\*hypeman.Instance, error)
- client.Instances.Standby(ctx context.Context, id string) (\*hypeman.Instance, error)
- client.Instances.Start(ctx context.Context, id string, body hypeman.InstanceStartParams) (\*hypeman.Instance, error)
- client.Instances.Stat(ctx context.Context, id string, query hypeman.InstanceStatParams) (\*hypeman.PathInfo, error)
+- client.Instances.Stats(ctx context.Context, id string) (\*hypeman.InstanceStats, error)
- client.Instances.Stop(ctx context.Context, id string) (\*hypeman.Instance, error)
## Volumes
diff --git a/instance.go b/instance.go
index 28cbf5b..cfe3b54 100644
--- a/instance.go
+++ b/instance.go
@@ -71,6 +71,18 @@ func (r *InstanceService) Delete(ctx context.Context, id string, opts ...option.
return
}
+// 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)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("instances/%s/fork", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
+ return
+}
+
// Get instance details
func (r *InstanceService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Instance, err error) {
opts = slices.Concat(r.Options, opts)
@@ -157,6 +169,20 @@ func (r *InstanceService) Stat(ctx context.Context, id string, query InstanceSta
return
}
+// Returns real-time resource utilization statistics for a running VM instance.
+// Metrics are collected from /proc//stat and /proc//statm for CPU and
+// memory, and from TAP interface statistics for network I/O.
+func (r *InstanceService) Stats(ctx context.Context, id string, opts ...option.RequestOption) (res *InstanceStats, err error) {
+ opts = slices.Concat(r.Options, opts)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("instances/%s/stats", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
// Stop instance (graceful shutdown)
func (r *InstanceService) Stop(ctx context.Context, id string, opts ...option.RequestOption) (res *Instance, err error) {
opts = slices.Concat(r.Options, opts)
@@ -348,6 +374,52 @@ func (r *InstanceNetwork) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
+// Real-time resource utilization statistics for a VM instance
+type InstanceStats struct {
+ // Total memory allocated to the VM (Size + HotplugSize) in bytes
+ AllocatedMemoryBytes int64 `json:"allocated_memory_bytes" api:"required"`
+ // Number of vCPUs allocated to the VM
+ AllocatedVcpus int64 `json:"allocated_vcpus" api:"required"`
+ // Total CPU time consumed by the VM hypervisor process in seconds
+ CPUSeconds float64 `json:"cpu_seconds" api:"required"`
+ // Instance identifier
+ InstanceID string `json:"instance_id" api:"required"`
+ // Instance name
+ InstanceName string `json:"instance_name" api:"required"`
+ // Resident Set Size - actual physical memory used by the VM in bytes
+ MemoryRssBytes int64 `json:"memory_rss_bytes" api:"required"`
+ // Virtual Memory Size - total virtual memory allocated in bytes
+ MemoryVmsBytes int64 `json:"memory_vms_bytes" api:"required"`
+ // Total network bytes received by the VM (from TAP interface)
+ NetworkRxBytes int64 `json:"network_rx_bytes" api:"required"`
+ // Total network bytes transmitted by the VM (from TAP interface)
+ NetworkTxBytes int64 `json:"network_tx_bytes" api:"required"`
+ // Memory utilization ratio (RSS / allocated memory). Only present when
+ // allocated_memory_bytes > 0.
+ MemoryUtilizationRatio float64 `json:"memory_utilization_ratio" api:"nullable"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ AllocatedMemoryBytes respjson.Field
+ AllocatedVcpus respjson.Field
+ CPUSeconds respjson.Field
+ InstanceID respjson.Field
+ InstanceName respjson.Field
+ MemoryRssBytes respjson.Field
+ MemoryVmsBytes respjson.Field
+ NetworkRxBytes respjson.Field
+ NetworkTxBytes respjson.Field
+ MemoryUtilizationRatio respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r InstanceStats) RawJSON() string { return r.JSON.raw }
+func (r *InstanceStats) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
type PathInfo struct {
// Whether the path exists
Exists bool `json:"exists" api:"required"`
@@ -590,6 +662,41 @@ const (
InstanceListParamsStateUnknown InstanceListParamsState = "Unknown"
)
+type InstanceForkParams struct {
+ // Name for the forked instance (lowercase letters, digits, and dashes only; cannot
+ // start or end with a dash)
+ Name string `json:"name" api:"required"`
+ // Allow forking from a running source instance. When true and source is Running,
+ // the source is put into standby, forked, then restored back to Running.
+ FromRunning param.Opt[bool] `json:"from_running,omitzero"`
+ // Optional final state for the forked instance. Default is the source instance
+ // state at fork time. For example, forking from Running defaults the fork result
+ // to Running.
+ //
+ // Any of "Stopped", "Standby", "Running".
+ TargetState InstanceForkParamsTargetState `json:"target_state,omitzero"`
+ paramObj
+}
+
+func (r InstanceForkParams) MarshalJSON() (data []byte, err error) {
+ type shadow InstanceForkParams
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *InstanceForkParams) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+// Optional final state for the forked instance. Default is the source instance
+// state at fork time. For example, forking from Running defaults the fork result
+// to Running.
+type InstanceForkParamsTargetState string
+
+const (
+ InstanceForkParamsTargetStateStopped InstanceForkParamsTargetState = "Stopped"
+ InstanceForkParamsTargetStateStandby InstanceForkParamsTargetState = "Standby"
+ InstanceForkParamsTargetStateRunning InstanceForkParamsTargetState = "Running"
+)
+
type InstanceLogsParams struct {
// Continue streaming new lines after initial output
Follow param.Opt[bool] `query:"follow,omitzero" json:"-"`
diff --git a/instance_test.go b/instance_test.go
index 4434e31..92f99ae 100644
--- a/instance_test.go
+++ b/instance_test.go
@@ -124,6 +124,37 @@ func TestInstanceDelete(t *testing.T) {
}
}
+func TestInstanceForkWithOptionalParams(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.Fork(
+ context.TODO(),
+ "id",
+ hypeman.InstanceForkParams{
+ Name: "my-workload-1-fork",
+ FromRunning: hypeman.Bool(false),
+ TargetState: hypeman.InstanceForkParamsTargetStateRunning,
+ },
+ )
+ 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 TestInstanceGet(t *testing.T) {
t.Skip("Mock server tests are disabled")
baseURL := "http://localhost:4010"
@@ -253,6 +284,29 @@ func TestInstanceStatWithOptionalParams(t *testing.T) {
}
}
+func TestInstanceStats(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.Stats(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 TestInstanceStop(t *testing.T) {
t.Skip("Mock server tests are disabled")
baseURL := "http://localhost:4010"
diff --git a/internal/version.go b/internal/version.go
index 870e575..1f338c3 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -2,4 +2,4 @@
package internal
-const PackageVersion = "0.14.0" // x-release-please-version
+const PackageVersion = "0.15.0" // x-release-please-version
diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh
new file mode 100755
index 0000000..37f5561
--- /dev/null
+++ b/scripts/utils/upload-artifact.sh
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+set -exuo pipefail
+
+DIST_DIR="dist"
+FILENAME="source.zip"
+
+mapfile -d '' files < <(
+ find . -type f \
+ \( -name '*.go' -o -name 'go.mod' -o -name 'go.sum' \) \
+ ! -path "./${DIST_DIR}/*" \
+ -print0
+)
+
+if [[ ${#files[@]} -eq 0 ]]; then
+ echo -e "\033[31mNo Go source files found for packaging.\033[0m"
+ exit 1
+fi
+
+mkdir -p "$DIST_DIR"
+rm -f "${DIST_DIR}/${FILENAME}"
+
+relative_files=()
+for file in "${files[@]}"; do
+ relative_files+=("${file#./}")
+done
+
+zip "${DIST_DIR}/${FILENAME}" "${relative_files[@]}"
+
+RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \
+ -H "Authorization: Bearer $AUTH" \
+ -H "Content-Type: application/json")
+
+SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url')
+
+if [[ "$SIGNED_URL" == "null" ]]; then
+ echo -e "\033[31mFailed to get signed URL.\033[0m"
+ exit 1
+fi
+
+UPLOAD_RESPONSE=$(curl -v -X PUT \
+ -H "Content-Type: application/zip" \
+ --data-binary "@${DIST_DIR}/${FILENAME}" "$SIGNED_URL" 2>&1)
+
+if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then
+ echo -e "\033[32mUploaded build to Stainless storage.\033[0m"
+ echo -e "\033[32mInstallation: Download and unzip: 'https://pkg.stainless.com/s/hypeman-go/$SHA'. Run 'go mod edit -replace github.com/kernel/hypeman-go=/path/to/unzipped_directory'.\033[0m"
+else
+ echo -e "\033[31mFailed to upload artifact.\033[0m"
+ exit 1
+fi