diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index 4fcaaf6..4a3c5a0 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -1,12 +1,21 @@
name: Release Please
+
+# `next` accumulates validated SDK changes in one versioned PR to `main`.
+# Merging that PR creates the GitHub release; the package publishing workflow
+# runs from the release event.
on:
push:
branches:
+ - next
- main
permissions:
contents: read
+concurrency:
+ group: release-please
+ cancel-in-progress: false
+
jobs:
release-please:
if: github.repository == 'kernel/hypeman-go'
@@ -26,7 +35,68 @@ jobs:
permission-pull-requests: write
permission-workflows: write
- - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1
- id: release
+ - name: Set up Node
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: '18.20.2'
+
+ - name: Set up pnpm
+ uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
with:
- token: ${{ steps.release-token.outputs.token }}
+ version: '9.11.0'
+ run_install: false
+
+ - name: Build pinned release tooling
+ id: tooling
+ env:
+ RELEASE_PLEASE_DIR: ${{ runner.temp }}/release-please
+ RELEASE_PLEASE_SHA: a116e1e520e0f87824acf46a2e79c91d41e819d7
+ run: |
+ set -euo pipefail
+ rm -rf "$RELEASE_PLEASE_DIR"
+ git init "$RELEASE_PLEASE_DIR"
+ git -C "$RELEASE_PLEASE_DIR" remote add origin https://github.com/stainless-api/release-please.git
+ git -C "$RELEASE_PLEASE_DIR" fetch --depth=1 origin "$RELEASE_PLEASE_SHA"
+ git -C "$RELEASE_PLEASE_DIR" checkout --detach FETCH_HEAD
+ pnpm --dir "$RELEASE_PLEASE_DIR" install --frozen-lockfile
+ pnpm --dir "$RELEASE_PLEASE_DIR" build
+ echo "cli=$RELEASE_PLEASE_DIR/build/src/bin/release-please.js" >> "$GITHUB_OUTPUT"
+
+ - name: Open or update the release PR
+ if: github.ref_name == 'next'
+ env:
+ GH_TOKEN: ${{ steps.release-token.outputs.token }}
+ RELEASE_PLEASE: ${{ steps.tooling.outputs.cli }}
+ run: |
+ set -euo pipefail
+ node "$RELEASE_PLEASE" release-pr \
+ --repo-url "$GITHUB_REPOSITORY" \
+ --token "$GH_TOKEN" \
+ --target-branch main \
+ --changes-branch next
+
+ - name: Remove the legacy promotion PR
+ if: github.ref_name == 'next'
+ env:
+ GH_TOKEN: ${{ steps.release-token.outputs.token }}
+ run: |
+ set -euo pipefail
+ legacy=$(gh pr list --repo "$GITHUB_REPOSITORY" --head stainless/release \
+ --state open --json number --jq '.[].number')
+ for pr in $legacy; do
+ gh pr close "$pr" --repo "$GITHUB_REPOSITORY" \
+ --comment "Superseded by the versioned release PR from next to main."
+ done
+ gh api -X DELETE "repos/$GITHUB_REPOSITORY/git/refs/heads/stainless/release" >/dev/null 2>&1 || true
+
+ - name: Create the GitHub release
+ if: github.ref_name == 'main'
+ env:
+ GH_TOKEN: ${{ steps.release-token.outputs.token }}
+ RELEASE_PLEASE: ${{ steps.tooling.outputs.cli }}
+ run: |
+ set -euo pipefail
+ node "$RELEASE_PLEASE" github-release \
+ --repo-url "$GITHUB_REPOSITORY" \
+ --token "$GH_TOKEN" \
+ --target-branch main
diff --git a/.github/workflows/stlc-promote.yml b/.github/workflows/stlc-promote.yml
index 0199324..fd9042d 100644
--- a/.github/workflows/stlc-promote.yml
+++ b/.github/workflows/stlc-promote.yml
@@ -1,8 +1,12 @@
-name: Promote SDKs
+name: Promote SDK changes
-# Manually fast-forwards production main to the reviewed staging main. The
-# ancestor check refuses divergent histories; this workflow never force-pushes.
+# Staging is the generator's integration history. Production `next` is the
+# developer-facing queue for the next release. This workflow combines the
+# latest released state with validated staging changes, then advances `next`.
+# Release automation maintains the single versioned PR from `next` to `main`.
on:
+ push:
+ branches: [main]
workflow_dispatch: {}
permissions:
@@ -12,7 +16,9 @@ jobs:
promote:
if: github.repository == 'kernel/hypeman-go-staging'
runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }}
- environment: production
+ concurrency:
+ group: stlc-promote
+ cancel-in-progress: true
steps:
- name: Check out staging
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -29,33 +35,99 @@ jobs:
owner: kernel
repositories: hypeman-go
permission-contents: write
+ permission-pull-requests: write
permission-workflows: write
- - name: Fetch production main
+ - name: Fetch production branches
+ id: production
env:
GH_TOKEN: ${{ steps.production-token.outputs.token }}
PRODUCTION_REPO: kernel/hypeman-go
run: |
- git remote add production "https://x-access-token:${GH_TOKEN}@github.com/${PRODUCTION_REPO}.git"
+ set -euo pipefail
+ git remote add production \
+ "https://x-access-token:${GH_TOKEN}@github.com/${PRODUCTION_REPO}.git"
git fetch production main
-
- - name: Check whether production already has staging's content
- id: diff
- run: |
- MERGED=$(git merge-tree --write-tree production/main origin/main) || MERGED=conflict
- PRODUCTION_TREE=$(git rev-parse 'production/main^{tree}')
- if [ "$MERGED" = "$PRODUCTION_TREE" ]; then
- echo "Production already contains staging's content. Nothing to promote."
- echo "synced=true" >> "$GITHUB_OUTPUT"
+ if git ls-remote --exit-code --heads production next >/dev/null 2>&1; then
+ git fetch production next
+ echo "has_next=true" >> "$GITHUB_OUTPUT"
else
- echo "synced=false" >> "$GITHUB_OUTPUT"
+ echo "has_next=false" >> "$GITHUB_OUTPUT"
fi
- - name: Promote staging to production
- if: steps.diff.outputs.synced == 'false'
+ - name: Prepare the next release branch
+ env:
+ APP_SLUG: ${{ steps.production-token.outputs.app-slug }}
+ GH_TOKEN: ${{ steps.production-token.outputs.token }}
+ HAS_NEXT: ${{ steps.production.outputs.has_next }}
+ PRODUCTION_REPO: kernel/hypeman-go
run: |
- if ! git merge-base --is-ancestor production/main origin/main; then
- echo "::error title=Promote blocked::production/main is not an ancestor of staging main. Back-sync production first."
+ set -euo pipefail
+ bot_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)
+ git config user.name "${APP_SLUG}[bot]"
+ git config user.email "${bot_id}+${APP_SLUG}[bot]@users.noreply.github.com"
+
+ open_conflict_pr() {
+ source_ref=$1
+ source_name=$2
+ advance_next=$3
+ conflict_branch=stlc/promotion-conflict
+
+ git merge --abort
+ existing=$(gh pr list --repo "$PRODUCTION_REPO" --base next \
+ --head "$conflict_branch" --state open --json url --jq '.[0].url // ""')
+ if [ -n "$existing" ]; then
+ echo "::error title=SDK promotion blocked::Resolve the existing recovery PR: $existing"
+ exit 1
+ fi
+
+ if [ "$advance_next" = "true" ]; then
+ git push production HEAD:refs/heads/next
+ fi
+ git push production "$source_ref:refs/heads/$conflict_branch" --force
+
+ body=$(mktemp)
+ printf '%s\n' \
+ '## SDK promotion conflict' \
+ '' \
+ "The automated promotion could not merge $source_name into the pending next release." \
+ '' \
+ 'Resolve the conflicts on this branch, validate the SDK, mark this PR ready, and merge it with a merge commit.' \
+ '' \
+ 'After merging, rerun the staging Promote SDK changes workflow to include any newer generated changes.' \
+ > "$body"
+ recovery_url=$(gh pr create --repo "$PRODUCTION_REPO" --draft \
+ --base next --head "$conflict_branch" \
+ --title 'chore: resolve SDK promotion conflict' --body-file "$body")
+ echo "::error title=SDK promotion conflict::Resolve the recovery PR: $recovery_url"
exit 1
+ }
+
+ if [ "$HAS_NEXT" = "true" ]; then
+ git checkout -B stlc/promote-next production/next
+ else
+ git checkout -B stlc/promote-next production/main
+ fi
+
+ if ! git merge-base --is-ancestor production/main HEAD; then
+ if ! git merge --no-edit production/main; then
+ open_conflict_pr production/main 'production main' false
+ fi
+ fi
+ if ! git merge-base --is-ancestor origin/main HEAD; then
+ if ! git merge --no-edit origin/main; then
+ open_conflict_pr origin/main 'validated staging changes' true
+ fi
+ fi
+
+ if [ "$HAS_NEXT" = "true" ]; then
+ git merge-base --is-ancestor production/next HEAD
fi
- git push production origin/main:refs/heads/main
+
+ - name: Update the pending release
+ env:
+ GH_TOKEN: ${{ steps.production-token.outputs.token }}
+ run: |
+ set -euo pipefail
+ git push production HEAD:refs/heads/next
+ echo "Updated production next; the versioned release PR will be opened or refreshed."
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 7f3f5c8..d2d60a3 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.23.0"
+ ".": "0.24.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index b4620cc..1c9b72d 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 58
+configured_endpoints: 61
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d177453..6d8da84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## [0.24.0](https://github.com/kernel/hypeman-go/compare/v0.23.0...v0.24.0) (2026-08-12)
+
+
+### Features
+
+* Add QEMU microvm hypervisor backend ([f8a5d45](https://github.com/kernel/hypeman-go/commit/f8a5d4586ff9c812b8b5f0aa342bb0b7b47bd3dc))
+* Add request header authorization to ingress rules ([fb25cdc](https://github.com/kernel/hypeman-go/commit/fb25cdc43a87da159d80da7560d87e300754907a))
+* chore(stlc): seal custom-code tracking files ([f89533d](https://github.com/kernel/hypeman-go/commit/f89533d396385abb4ea110bb33266e006f44a46a))
+
## 0.23.0 (2026-08-06)
Full Changelog: [v0.22.0...v0.23.0](https://github.com/kernel/hypeman-go/compare/v0.22.0...v0.23.0)
diff --git a/README.md b/README.md
index 77705c3..cfeb011 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.23.0'
+go get -u 'github.com/kernel/hypeman-go@v0.24.0'
```
diff --git a/api.md b/api.md
index dfe4a3d..0776586 100644
--- a/api.md
+++ b/api.md
@@ -236,3 +236,21 @@ Methods:
- client.Builds.Cancel(ctx context.Context, id string) error
- client.Builds.Events(ctx context.Context, id string, query hypeman.BuildEventsParams) (\*hypeman.BuildEvent, error)
- client.Builds.Get(ctx context.Context, id string) (\*hypeman.Build, error)
+
+# Pushes
+
+Params Types:
+
+- hypeman.CreatePushRequestParam
+- hypeman.PushCredentialsParam
+
+Response Types:
+
+- hypeman.Push
+- hypeman.PushStatus
+
+Methods:
+
+- client.Pushes.New(ctx context.Context, body hypeman.PushNewParams) (\*hypeman.Push, error)
+- client.Pushes.List(ctx context.Context) (\*[]hypeman.Push, error)
+- client.Pushes.Get(ctx context.Context, id string) (\*hypeman.Push, error)
diff --git a/client.go b/client.go
index 59b3749..ce1422c 100644
--- a/client.go
+++ b/client.go
@@ -28,6 +28,7 @@ type Client struct {
Resources ResourceService
Builders BuilderService
Builds BuildService
+ Pushes PushService
}
// DefaultClientOptions read from the environment (HYPEMAN_API_KEY,
@@ -70,6 +71,7 @@ func NewClient(opts ...option.RequestOption) (r Client) {
r.Resources = NewResourceService(opts...)
r.Builders = NewBuilderService(opts...)
r.Builds = NewBuildService(opts...)
+ r.Pushes = NewPushService(opts...)
return
}
diff --git a/ingress.go b/ingress.go
index 8d4393d..46ded56 100644
--- a/ingress.go
+++ b/ingress.go
@@ -174,17 +174,19 @@ type IngressRule struct {
Target IngressTarget `json:"target" api:"required"`
// Auto-create HTTP to HTTPS redirect for this hostname (only applies when tls is
// enabled)
- RedirectHTTP bool `json:"redirect_http"`
+ RedirectHTTP bool `json:"redirect_http"`
+ RequestHeaderAuth IngressRuleRequestHeaderAuth `json:"request_header_auth"`
// Enable TLS termination (certificate auto-issued via ACME).
Tls bool `json:"tls"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
- Match respjson.Field
- Target respjson.Field
- RedirectHTTP respjson.Field
- Tls respjson.Field
- ExtraFields map[string]respjson.Field
- raw string
+ Match respjson.Field
+ Target respjson.Field
+ RedirectHTTP respjson.Field
+ RequestHeaderAuth respjson.Field
+ Tls respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
} `json:"-"`
}
@@ -203,6 +205,30 @@ func (r IngressRule) ToParam() IngressRuleParam {
return param.Override[IngressRuleParam](json.RawMessage(r.RawJSON()))
}
+type IngressRuleRequestHeaderAuth struct {
+ // Dedicated request header that must match before proxying. Reserved
+ // authentication, cookie, host, framing, proxy, and hop-by-hop headers are not
+ // allowed.
+ Header string `json:"header" api:"required"`
+ // Exact header value required before proxying. This sensitive value is persisted
+ // and returned by the API like instance environment variables; clients should hide
+ // it by default.
+ Value string `json:"value" api:"required"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ Header respjson.Field
+ Value respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r IngressRuleRequestHeaderAuth) RawJSON() string { return r.JSON.raw }
+func (r *IngressRuleRequestHeaderAuth) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
// The properties Match, Target are required.
type IngressRuleParam struct {
Match IngressMatchParam `json:"match,omitzero" api:"required"`
@@ -211,7 +237,8 @@ type IngressRuleParam struct {
// enabled)
RedirectHTTP param.Opt[bool] `json:"redirect_http,omitzero"`
// Enable TLS termination (certificate auto-issued via ACME).
- Tls param.Opt[bool] `json:"tls,omitzero"`
+ Tls param.Opt[bool] `json:"tls,omitzero"`
+ RequestHeaderAuth IngressRuleRequestHeaderAuthParam `json:"request_header_auth,omitzero"`
paramObj
}
@@ -223,6 +250,27 @@ func (r *IngressRuleParam) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
+// The properties Header, Value are required.
+type IngressRuleRequestHeaderAuthParam struct {
+ // Dedicated request header that must match before proxying. Reserved
+ // authentication, cookie, host, framing, proxy, and hop-by-hop headers are not
+ // allowed.
+ Header string `json:"header" api:"required"`
+ // Exact header value required before proxying. This sensitive value is persisted
+ // and returned by the API like instance environment variables; clients should hide
+ // it by default.
+ Value string `json:"value" api:"required"`
+ paramObj
+}
+
+func (r IngressRuleRequestHeaderAuthParam) MarshalJSON() (data []byte, err error) {
+ type shadow IngressRuleRequestHeaderAuthParam
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *IngressRuleRequestHeaderAuthParam) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
type IngressTarget struct {
// Target instance name, ID, or capture reference.
//
diff --git a/ingress_test.go b/ingress_test.go
index 68693c9..5671c96 100644
--- a/ingress_test.go
+++ b/ingress_test.go
@@ -38,7 +38,11 @@ func TestIngressNewWithOptionalParams(t *testing.T) {
Port: 8080,
},
RedirectHTTP: hypeman.Bool(true),
- Tls: hypeman.Bool(true),
+ RequestHeaderAuth: hypeman.IngressRuleRequestHeaderAuthParam{
+ Header: "X-Ingress-Verification",
+ Value: "0123456789abcdef0123456789abcdef",
+ },
+ Tls: hypeman.Bool(true),
}},
Tags: map[string]string{
"team": "backend",
diff --git a/instance.go b/instance.go
index 6be329d..50e5b37 100644
--- a/instance.go
+++ b/instance.go
@@ -679,9 +679,9 @@ type Instance struct {
HealthStatus InstanceHealthStatus `json:"health_status"`
// Hotplug memory size (human-readable)
HotplugSize string `json:"hotplug_size"`
- // Hypervisor running this instance
+ // Hypervisor backend running this instance
//
- // Any of "cloud-hypervisor", "firecracker", "qemu", "vz".
+ // Any of "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", "vz".
Hypervisor InstanceHypervisor `json:"hypervisor"`
// Network configuration of the instance
Network InstanceNetwork `json:"network"`
@@ -804,13 +804,14 @@ func (r *InstanceGPU) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
-// Hypervisor running this instance
+// Hypervisor backend running this instance
type InstanceHypervisor string
const (
InstanceHypervisorCloudHypervisor InstanceHypervisor = "cloud-hypervisor"
InstanceHypervisorFirecracker InstanceHypervisor = "firecracker"
InstanceHypervisorQemu InstanceHypervisor = "qemu"
+ InstanceHypervisorQemuMicrovm InstanceHypervisor = "qemu-microvm"
InstanceHypervisorVz InstanceHypervisor = "vz"
)
@@ -1454,9 +1455,12 @@ type InstanceNewParams struct {
// Workload health check policy. Health is reported separately from instance
// lifecycle state.
HealthCheck HealthCheckParam `json:"health_check,omitzero"`
- // Hypervisor to use for this instance. Defaults to server configuration.
+ // Hypervisor backend to use for this instance. qemu uses the architecture-native
+ // standard board; qemu-microvm uses QEMU's minimal Linux amd64 board and does not
+ // support PCI devices, hotplug memory, or more than eight virtio-mmio devices.
+ // Defaults to server configuration.
//
- // Any of "cloud-hypervisor", "firecracker", "qemu", "vz".
+ // Any of "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", "vz".
Hypervisor InstanceNewParamsHypervisor `json:"hypervisor,omitzero"`
// Network configuration for the instance
Network InstanceNewParamsNetwork `json:"network,omitzero"`
@@ -1567,13 +1571,17 @@ func (r *InstanceNewParamsGPU) UnmarshalJSON(data []byte) error {
return apijson.UnmarshalRoot(data, r)
}
-// Hypervisor to use for this instance. Defaults to server configuration.
+// Hypervisor backend to use for this instance. qemu uses the architecture-native
+// standard board; qemu-microvm uses QEMU's minimal Linux amd64 board and does not
+// support PCI devices, hotplug memory, or more than eight virtio-mmio devices.
+// Defaults to server configuration.
type InstanceNewParamsHypervisor string
const (
InstanceNewParamsHypervisorCloudHypervisor InstanceNewParamsHypervisor = "cloud-hypervisor"
InstanceNewParamsHypervisorFirecracker InstanceNewParamsHypervisor = "firecracker"
InstanceNewParamsHypervisorQemu InstanceNewParamsHypervisor = "qemu"
+ InstanceNewParamsHypervisorQemuMicrovm InstanceNewParamsHypervisor = "qemu-microvm"
InstanceNewParamsHypervisorVz InstanceNewParamsHypervisor = "vz"
)
diff --git a/instancesnapshot.go b/instancesnapshot.go
index d0aed99..97e5089 100644
--- a/instancesnapshot.go
+++ b/instancesnapshot.go
@@ -92,7 +92,7 @@ type InstanceSnapshotRestoreParams struct {
// Optional hypervisor override. Allowed only when restoring from a Stopped
// snapshot. Standby snapshots must restore with their original hypervisor.
//
- // Any of "cloud-hypervisor", "firecracker", "qemu", "vz".
+ // Any of "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", "vz".
TargetHypervisor InstanceSnapshotRestoreParamsTargetHypervisor `json:"target_hypervisor,omitzero"`
// Optional final state after restore. Defaults by snapshot kind:
//
@@ -120,6 +120,7 @@ const (
InstanceSnapshotRestoreParamsTargetHypervisorCloudHypervisor InstanceSnapshotRestoreParamsTargetHypervisor = "cloud-hypervisor"
InstanceSnapshotRestoreParamsTargetHypervisorFirecracker InstanceSnapshotRestoreParamsTargetHypervisor = "firecracker"
InstanceSnapshotRestoreParamsTargetHypervisorQemu InstanceSnapshotRestoreParamsTargetHypervisor = "qemu"
+ InstanceSnapshotRestoreParamsTargetHypervisorQemuMicrovm InstanceSnapshotRestoreParamsTargetHypervisor = "qemu-microvm"
InstanceSnapshotRestoreParamsTargetHypervisorVz InstanceSnapshotRestoreParamsTargetHypervisor = "vz"
)
diff --git a/internal/version.go b/internal/version.go
index 834f803..5f694dc 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -2,4 +2,4 @@
package internal
-const PackageVersion = "0.23.0" // x-release-please-version
+const PackageVersion = "0.24.0" // x-release-please-version
diff --git a/push.go b/push.go
new file mode 100644
index 0000000..3c74de5
--- /dev/null
+++ b/push.go
@@ -0,0 +1,176 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package hypeman
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "slices"
+ "time"
+
+ "github.com/kernel/hypeman-go/internal/apijson"
+ shimjson "github.com/kernel/hypeman-go/internal/encoding/json"
+ "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"
+)
+
+// PushService 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 [NewPushService] method instead.
+type PushService struct {
+ Options []option.RequestOption
+}
+
+// NewPushService 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 NewPushService(opts ...option.RequestOption) (r PushService) {
+ r = PushService{}
+ r.Options = opts
+ return
+}
+
+// Creates a push job that exports a hypeman image from the local OCI cache to a
+// remote registry (e.g. AWS ECR, Docker Hub). Only images in the ready state can
+// be pushed.
+func (r *PushService) New(ctx context.Context, body PushNewParams, opts ...option.RequestOption) (res *Push, err error) {
+ opts = slices.Concat(r.Options, opts)
+ path := "pushes"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
+ return res, err
+}
+
+// Lists outbound image push jobs, newest first.
+func (r *PushService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Push, err error) {
+ opts = slices.Concat(r.Options, opts)
+ path := "pushes"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return res, err
+}
+
+// Get push details
+func (r *PushService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Push, err error) {
+ opts = slices.Concat(r.Options, opts)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return nil, err
+ }
+ path := fmt.Sprintf("pushes/%s", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return res, err
+}
+
+// The properties Image, Target are required.
+type CreatePushRequestParam struct {
+ // Hypeman image name to push (tag or digest form)
+ Image string `json:"image" api:"required"`
+ // Full remote reference to push to
+ Target string `json:"target" api:"required"`
+ // Allow pushing to plain-HTTP registries
+ Insecure param.Opt[bool] `json:"insecure,omitzero"`
+ // Registry credentials borrowed for this push only. When omitted, the server's own
+ // registry credentials are used.
+ Credentials PushCredentialsParam `json:"credentials,omitzero"`
+ paramObj
+}
+
+func (r CreatePushRequestParam) MarshalJSON() (data []byte, err error) {
+ type shadow CreatePushRequestParam
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *CreatePushRequestParam) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type Push struct {
+ // Push job identifier
+ ID string `json:"id" api:"required"`
+ CreatedAt time.Time `json:"created_at" api:"required" format:"date-time"`
+ // Cached manifest digest being pushed
+ Digest string `json:"digest" api:"required"`
+ // Hypeman image name (normalized ref)
+ Image string `json:"image" api:"required"`
+ // Any of "queued", "pushing", "pushed", "failed".
+ Status PushStatus `json:"status" api:"required"`
+ // Remote reference the image is pushed to
+ Target string `json:"target" api:"required"`
+ // Total compressed layer bytes pushed
+ Bytes int64 `json:"bytes"`
+ CompletedAt time.Time `json:"completed_at" api:"nullable" format:"date-time"`
+ // Error message
+ Error string `json:"error" api:"nullable"`
+ // Number of layers pushed
+ Layers int64 `json:"layers"`
+ // Position in the push queue
+ QueuePosition int64 `json:"queue_position" api:"nullable"`
+ // JSON contains metadata for fields, check presence with [respjson.Field.Valid].
+ JSON struct {
+ ID respjson.Field
+ CreatedAt respjson.Field
+ Digest respjson.Field
+ Image respjson.Field
+ Status respjson.Field
+ Target respjson.Field
+ Bytes respjson.Field
+ CompletedAt respjson.Field
+ Error respjson.Field
+ Layers respjson.Field
+ QueuePosition respjson.Field
+ ExtraFields map[string]respjson.Field
+ raw string
+ } `json:"-"`
+}
+
+// Returns the unmodified JSON received from the API
+func (r Push) RawJSON() string { return r.JSON.raw }
+func (r *Push) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+// Registry credentials borrowed for this push only. When omitted, the server's own
+// registry credentials are used.
+type PushCredentialsParam struct {
+ // Registry password or access token
+ Password param.Opt[string] `json:"password,omitzero" format:"password"`
+ // Bearer token for an Authorization header
+ RegistryToken param.Opt[string] `json:"registry_token,omitzero" format:"password"`
+ // Registry username
+ Username param.Opt[string] `json:"username,omitzero"`
+ paramObj
+}
+
+func (r PushCredentialsParam) MarshalJSON() (data []byte, err error) {
+ type shadow PushCredentialsParam
+ return param.MarshalObject(r, (*shadow)(&r))
+}
+func (r *PushCredentialsParam) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+type PushStatus string
+
+const (
+ PushStatusQueued PushStatus = "queued"
+ PushStatusPushing PushStatus = "pushing"
+ PushStatusPushed PushStatus = "pushed"
+ PushStatusFailed PushStatus = "failed"
+)
+
+type PushNewParams struct {
+ CreatePushRequest CreatePushRequestParam
+ paramObj
+}
+
+func (r PushNewParams) MarshalJSON() (data []byte, err error) {
+ return shimjson.Marshal(r.CreatePushRequest)
+}
+func (r *PushNewParams) UnmarshalJSON(data []byte) error {
+ return apijson.UnmarshalRoot(data, r)
+}
diff --git a/push_test.go b/push_test.go
new file mode 100644
index 0000000..b5c7d37
--- /dev/null
+++ b/push_test.go
@@ -0,0 +1,94 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package hypeman_test
+
+import (
+ "context"
+ "errors"
+ "os"
+ "testing"
+
+ "github.com/kernel/hypeman-go"
+ "github.com/kernel/hypeman-go/internal/testutil"
+ "github.com/kernel/hypeman-go/option"
+)
+
+func TestPushNewWithOptionalParams(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.Pushes.New(context.TODO(), hypeman.PushNewParams{
+ CreatePushRequest: hypeman.CreatePushRequestParam{
+ Image: "docker.io/library/alpine:latest",
+ Target: "123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:v1",
+ Credentials: hypeman.PushCredentialsParam{
+ Password: hypeman.String("password"),
+ RegistryToken: hypeman.String("registry_token"),
+ Username: hypeman.String("username"),
+ },
+ Insecure: hypeman.Bool(true),
+ },
+ })
+ 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 TestPushList(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.Pushes.List(context.TODO())
+ 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 TestPushGet(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.Pushes.Get(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())
+ }
+}
diff --git a/release-please-config.json b/release-please-config.json
index 058b84a..bfddad6 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -10,6 +10,7 @@
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": false,
"pull-request-header": "Automated Release PR",
+ "pull-request-footer": "Merge this pull request with a merge commit. Merging creates the GitHub release and publishes the package.",
"pull-request-title-pattern": "release: ${version}",
"changelog-sections": [
{
diff --git a/resource.go b/resource.go
index 8177cbe..e8b34c2 100644
--- a/resource.go
+++ b/resource.go
@@ -148,7 +148,7 @@ const (
type MemoryReclaimAction struct {
AppliedReclaimBytes int64 `json:"applied_reclaim_bytes" api:"required"`
AssignedMemoryBytes int64 `json:"assigned_memory_bytes" api:"required"`
- // Any of "cloud-hypervisor", "firecracker", "qemu", "vz".
+ // Any of "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", "vz".
Hypervisor MemoryReclaimActionHypervisor `json:"hypervisor" api:"required"`
InstanceID string `json:"instance_id" api:"required"`
InstanceName string `json:"instance_name" api:"required"`
@@ -190,6 +190,7 @@ const (
MemoryReclaimActionHypervisorCloudHypervisor MemoryReclaimActionHypervisor = "cloud-hypervisor"
MemoryReclaimActionHypervisorFirecracker MemoryReclaimActionHypervisor = "firecracker"
MemoryReclaimActionHypervisorQemu MemoryReclaimActionHypervisor = "qemu"
+ MemoryReclaimActionHypervisorQemuMicrovm MemoryReclaimActionHypervisor = "qemu-microvm"
MemoryReclaimActionHypervisorVz MemoryReclaimActionHypervisor = "vz"
)
diff --git a/snapshot.go b/snapshot.go
index f615439..df58796 100644
--- a/snapshot.go
+++ b/snapshot.go
@@ -97,7 +97,7 @@ type Snapshot struct {
SizeBytes int64 `json:"size_bytes" api:"required"`
// Source instance hypervisor at snapshot creation time
//
- // Any of "cloud-hypervisor", "firecracker", "qemu", "vz".
+ // Any of "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", "vz".
SourceHypervisor SnapshotSourceHypervisor `json:"source_hypervisor" api:"required"`
// Source instance ID at snapshot creation time
SourceInstanceID string `json:"source_instance_id" api:"required"`
@@ -152,6 +152,7 @@ const (
SnapshotSourceHypervisorCloudHypervisor SnapshotSourceHypervisor = "cloud-hypervisor"
SnapshotSourceHypervisorFirecracker SnapshotSourceHypervisor = "firecracker"
SnapshotSourceHypervisorQemu SnapshotSourceHypervisor = "qemu"
+ SnapshotSourceHypervisorQemuMicrovm SnapshotSourceHypervisor = "qemu-microvm"
SnapshotSourceHypervisorVz SnapshotSourceHypervisor = "vz"
)
@@ -202,7 +203,7 @@ type SnapshotForkParams struct {
// Optional hypervisor override. Allowed only when forking from a Stopped snapshot.
// Standby snapshots must fork with their original hypervisor.
//
- // Any of "cloud-hypervisor", "firecracker", "qemu", "vz".
+ // Any of "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", "vz".
TargetHypervisor SnapshotForkParamsTargetHypervisor `json:"target_hypervisor,omitzero"`
// Optional final state for the forked instance. Defaults by snapshot kind:
//
@@ -230,6 +231,7 @@ const (
SnapshotForkParamsTargetHypervisorCloudHypervisor SnapshotForkParamsTargetHypervisor = "cloud-hypervisor"
SnapshotForkParamsTargetHypervisorFirecracker SnapshotForkParamsTargetHypervisor = "firecracker"
SnapshotForkParamsTargetHypervisorQemu SnapshotForkParamsTargetHypervisor = "qemu"
+ SnapshotForkParamsTargetHypervisorQemuMicrovm SnapshotForkParamsTargetHypervisor = "qemu-microvm"
SnapshotForkParamsTargetHypervisorVz SnapshotForkParamsTargetHypervisor = "vz"
)