Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 30
configured_endpoints: 36
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-e052ac01c788e7e3e46c96bf3c42be7ae57f9dd046129add8012d0eeb388e884.yml
openapi_spec_hash: fd805921c0162d63405f5feb7e8c7082
config_hash: 170041ea532e81620e0f6a73f6b31d44
config_hash: 14135b7c88169a15762c8defb0bdfd16
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

## 0.9.0 (2026-01-05)

Full Changelog: [v0.8.0...v0.9.0](https://github.com/onkernel/hypeman-go/compare/v0.8.0...v0.9.0)
Full Changelog: [v0.8.0...v0.9.0](https://github.com/kernel/hypeman-go/compare/v0.8.0...v0.9.0)

### Features

* QEMU support ([d708091](https://github.com/onkernel/hypeman-go/commit/d70809169d136df3f1efbf961f2a90084e1f9fa5))
* Resource accounting ([4141287](https://github.com/onkernel/hypeman-go/commit/414128770e8137ed2a40d404f0f4ac06ea1a0731))
* QEMU support ([d708091](https://github.com/kernel/hypeman-go/commit/d70809169d136df3f1efbf961f2a90084e1f9fa5))
* Resource accounting ([4141287](https://github.com/kernel/hypeman-go/commit/414128770e8137ed2a40d404f0f4ac06ea1a0731))

## 0.8.0 (2025-12-23)

Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Or to pin the version:
<!-- x-release-please-start-version -->

```sh
go get -u 'github.com/onkernel/hypeman-go@v0.9.0'
go get -u 'github.com/kernel/hypeman-go@v0.9.0'
```

<!-- x-release-please-end -->
Expand Down Expand Up @@ -342,6 +342,24 @@ file returned by `os.Open` will be sent with the file name on disk.
We also provide a helper `hypeman.File(reader io.Reader, filename string, contentType string)`
which can be used to wrap any `io.Reader` with the appropriate file name and content type.

```go
// A file from the file system
file, err := os.Open("/path/to/file")
hypeman.BuildNewParams{
Source: file,
}

// A file from a string
hypeman.BuildNewParams{
Source: strings.NewReader("my file contents"),
}

// With a custom filename and contentType
hypeman.BuildNewParams{
Source: hypeman.File(strings.NewReader(`{"hello": "foo"}`), "file.go", "application/json"),
}
```

### Retries

Certain errors will be automatically retried 2 times by default, with a short exponential backoff.
Expand Down
32 changes: 32 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,35 @@ Methods:
- <code title="get /ingresses">client.Ingresses.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#IngressService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Ingress">Ingress</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="delete /ingresses/{id}">client.Ingresses.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#IngressService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) <a href="https://pkg.go.dev/builtin#error">error</a></code>
- <code title="get /ingresses/{id}">client.Ingresses.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#IngressService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Ingress">Ingress</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# Resources

Response Types:

- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#DiskBreakdown">DiskBreakdown</a>
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#GPUProfile">GPUProfile</a>
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#GPUResourceStatus">GPUResourceStatus</a>
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#PassthroughDevice">PassthroughDevice</a>
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#ResourceAllocation">ResourceAllocation</a>
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#ResourceStatus">ResourceStatus</a>
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Resources">Resources</a>

Methods:

- <code title="get /resources">client.Resources.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#ResourceService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Resources">Resources</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>

# Builds

Response Types:

- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Build">Build</a>
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#BuildEvent">BuildEvent</a>
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#BuildProvenance">BuildProvenance</a>
- <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#BuildStatus">BuildStatus</a>

Methods:

- <code title="post /builds">client.Builds.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#BuildService.New">New</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#BuildNewParams">BuildNewParams</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Build">Build</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /builds">client.Builds.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#BuildService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (\*[]<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Build">Build</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="delete /builds/{id}">client.Builds.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#BuildService.Cancel">Cancel</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) <a href="https://pkg.go.dev/builtin#error">error</a></code>
- <code title="get /builds/{id}/events">client.Builds.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#BuildService.Events">Events</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, query <a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#BuildEventsParams">BuildEventsParams</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#BuildEvent">BuildEvent</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /builds/{id}">client.Builds.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#BuildService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (\*<a href="https://pkg.go.dev/github.com/kernel/hypeman-go">hypeman</a>.<a href="https://pkg.go.dev/github.com/kernel/hypeman-go#Build">Build</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
287 changes: 287 additions & 0 deletions build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

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"
"github.com/kernel/hypeman-go/packages/respjson"
"github.com/kernel/hypeman-go/packages/ssestream"
)

// BuildService contains methods and other services that help with interacting with
// the hypeman API.
//
// Note, unlike clients, this service does not read variables from the environment
// automatically. You should not instantiate this service directly, and instead use
// the [NewBuildService] method instead.
type BuildService struct {
Options []option.RequestOption
}

// NewBuildService generates a new service that applies the given options to each
// request. These options are applied after the parent client's options (if there
// is one), and before any request-specific options.
func NewBuildService(opts ...option.RequestOption) (r BuildService) {
r = BuildService{}
r.Options = opts
return
}

// Creates a new build job. Source code should be uploaded as a tar.gz archive in
// the multipart form data.
func (r *BuildService) New(ctx context.Context, body BuildNewParams, opts ...option.RequestOption) (res *Build, err error) {
opts = slices.Concat(r.Options, opts)
path := "builds"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
return
}

// List builds
func (r *BuildService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Build, err error) {
opts = slices.Concat(r.Options, opts)
path := "builds"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
return
}

// Cancel build
func (r *BuildService) Cancel(ctx context.Context, id string, opts ...option.RequestOption) (err error) {
opts = slices.Concat(r.Options, opts)
opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...)
if id == "" {
err = errors.New("missing required id parameter")
return
}
path := fmt.Sprintf("builds/%s", id)
err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...)
return
}

// Streams build events as Server-Sent Events. Events include:
//
// - `log`: Build log lines with timestamp and content
// - `status`: Build status changes (queued→building→pushing→ready/failed)
// - `heartbeat`: Keep-alive events sent every 30s to prevent connection timeouts
//
// Returns existing logs as events, then continues streaming if follow=true.
func (r *BuildService) EventsStreaming(ctx context.Context, id string, query BuildEventsParams, opts ...option.RequestOption) (stream *ssestream.Stream[BuildEvent]) {
var (
raw *http.Response
err error
)
opts = slices.Concat(r.Options, opts)
opts = append([]option.RequestOption{option.WithHeader("Accept", "text/event-stream")}, opts...)
if id == "" {
err = errors.New("missing required id parameter")
return
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early return discards error and returns nil stream

High Severity

In EventsStreaming, when id is empty, the function sets err but then does a bare return, causing the named return value stream to be nil. The error is stored in a local variable but never propagated to the caller. The intended pattern (seen in the final return statement) is to create a stream that wraps the error via ssestream.NewStream. Callers receiving a nil stream will experience a nil pointer panic when trying to use it, and have no way to detect that an error occurred.

Fix in Cursor Fix in Web

path := fmt.Sprintf("builds/%s/events", id)
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &raw, opts...)
return ssestream.NewStream[BuildEvent](ssestream.NewDecoder(raw), err)
}

// Get build details
func (r *BuildService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Build, err error) {
opts = slices.Concat(r.Options, opts)
if id == "" {
err = errors.New("missing required id parameter")
return
}
path := fmt.Sprintf("builds/%s", id)
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
return
}

type Build struct {
// Build job identifier
ID string `json:"id,required"`
// Build creation timestamp
CreatedAt time.Time `json:"created_at,required" format:"date-time"`
// Build job status
//
// Any of "queued", "building", "pushing", "ready", "failed", "cancelled".
Status BuildStatus `json:"status,required"`
// Build completion timestamp
CompletedAt time.Time `json:"completed_at,nullable" format:"date-time"`
// Build duration in milliseconds
DurationMs int64 `json:"duration_ms,nullable"`
// Error message (only when status is failed)
Error string `json:"error,nullable"`
// Digest of built image (only when status is ready)
ImageDigest string `json:"image_digest,nullable"`
// Full image reference (only when status is ready)
ImageRef string `json:"image_ref,nullable"`
Provenance BuildProvenance `json:"provenance"`
// Position in build queue (only when status is queued)
QueuePosition int64 `json:"queue_position,nullable"`
// Build start timestamp
StartedAt time.Time `json:"started_at,nullable" format:"date-time"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
ID respjson.Field
CreatedAt respjson.Field
Status respjson.Field
CompletedAt respjson.Field
DurationMs respjson.Field
Error respjson.Field
ImageDigest respjson.Field
ImageRef respjson.Field
Provenance respjson.Field
QueuePosition respjson.Field
StartedAt respjson.Field
ExtraFields map[string]respjson.Field
raw string
} `json:"-"`
}

// Returns the unmodified JSON received from the API
func (r Build) RawJSON() string { return r.JSON.raw }
func (r *Build) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}

type BuildEvent struct {
// Event timestamp
Timestamp time.Time `json:"timestamp,required" format:"date-time"`
// Event type
//
// Any of "log", "status", "heartbeat".
Type BuildEventType `json:"type,required"`
// Log line content (only for type=log)
Content string `json:"content"`
// New build status (only for type=status)
//
// Any of "queued", "building", "pushing", "ready", "failed", "cancelled".
Status BuildStatus `json:"status"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
Timestamp respjson.Field
Type respjson.Field
Content respjson.Field
Status respjson.Field
ExtraFields map[string]respjson.Field
raw string
} `json:"-"`
}

// Returns the unmodified JSON received from the API
func (r BuildEvent) RawJSON() string { return r.JSON.raw }
func (r *BuildEvent) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}

// Event type
type BuildEventType string

const (
BuildEventTypeLog BuildEventType = "log"
BuildEventTypeStatus BuildEventType = "status"
BuildEventTypeHeartbeat BuildEventType = "heartbeat"
)

type BuildProvenance struct {
// Pinned base image digest used
BaseImageDigest string `json:"base_image_digest"`
// BuildKit version used
BuildkitVersion string `json:"buildkit_version"`
// Map of lockfile names to SHA256 hashes
LockfileHashes map[string]string `json:"lockfile_hashes"`
// SHA256 hash of source tarball
SourceHash string `json:"source_hash"`
// Build completion timestamp
Timestamp time.Time `json:"timestamp" format:"date-time"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
BaseImageDigest respjson.Field
BuildkitVersion respjson.Field
LockfileHashes respjson.Field
SourceHash respjson.Field
Timestamp respjson.Field
ExtraFields map[string]respjson.Field
raw string
} `json:"-"`
}

// Returns the unmodified JSON received from the API
func (r BuildProvenance) RawJSON() string { return r.JSON.raw }
func (r *BuildProvenance) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}

// Build job status
type BuildStatus string

const (
BuildStatusQueued BuildStatus = "queued"
BuildStatusBuilding BuildStatus = "building"
BuildStatusPushing BuildStatus = "pushing"
BuildStatusReady BuildStatus = "ready"
BuildStatusFailed BuildStatus = "failed"
BuildStatusCancelled BuildStatus = "cancelled"
)

type BuildNewParams struct {
// Source tarball (tar.gz) containing application code and optionally a Dockerfile
Source io.Reader `json:"source,omitzero,required" format:"binary"`
// Optional pinned base image digest
BaseImageDigest param.Opt[string] `json:"base_image_digest,omitzero"`
// Tenant-specific cache key prefix
CacheScope param.Opt[string] `json:"cache_scope,omitzero"`
// Dockerfile content. Required if not included in the source tarball.
Dockerfile param.Opt[string] `json:"dockerfile,omitzero"`
// JSON array of secret references to inject during build. Each object has "id"
// (required) for use with --mount=type=secret,id=... Example: [{"id":
// "npm_token"}, {"id": "github_token"}]
Secrets param.Opt[string] `json:"secrets,omitzero"`
// Build timeout (default 600)
TimeoutSeconds param.Opt[int64] `json:"timeout_seconds,omitzero"`
paramObj
}

func (r BuildNewParams) 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
}

type BuildEventsParams struct {
// Continue streaming new events after initial output
Follow param.Opt[bool] `query:"follow,omitzero" json:"-"`
paramObj
}

// URLQuery serializes [BuildEventsParams]'s query parameters as `url.Values`.
func (r BuildEventsParams) URLQuery() (v url.Values, err error) {
return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
ArrayFormat: apiquery.ArrayQueryFormatComma,
NestedFormat: apiquery.NestedQueryFormatBrackets,
})
}
Loading