Skip to content

api: add /builders CRUD and prune endpoints - #341

Open
rgarcia wants to merge 1 commit into
hypeship/builders-domainfrom
hypeship/builders-api
Open

api: add /builders CRUD and prune endpoints#341
rgarcia wants to merge 1 commit into
hypeship/builders-domainfrom
hypeship/builders-api

Conversation

@rgarcia

@rgarcia rgarcia commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Expose the Builder resource over the API. POST /builders creates a
builder with an optional caller-supplied ID (validated, conflicts on
replay for control-plane idempotency), optional non-unique name, tags,
and disk size; the cache disk is provisioned eagerly and 201 returns the
builder. GET /builders lists with tag filtering and always returns a
non-nil array; GET /builders/{id} returns one builder; DELETE
/builders/{id} removes builder and disk (409 while in use); POST
/builders/{id}/prune resets the cache disk asynchronously and returns
202 with status pruning. Responses carry id, name, disk_size_gb, status,
tags, created_at, and last_used_at only. Every builder route documents
the 401 bearer-auth failure response.

Builders resolve by opaque ID through the resource resolver middleware;
builder:read/write/delete scopes gate the routes. The builders manager
is wired through config (builders.max_count, default_disk_size_gb,
max_disk_size_gb, idle_ttl), providers, and the generated injector, and
started with the server so startup reconciliation runs. Regenerating
oapi.go renamed the ImageStatus enum constants (oapi-codegen conflict
resolution with the new BuilderStatus enum); the one referencing test is
updated.

Handler tests cover create (defaults, custom ID, invalid ID, idempotent
replay), get, list with tag filtering, delete, prune, and 409s while a
build holds the builder. Config validation tests cover the new
builders.* settings.


Stack created with GitHub Stacks CLIGive Feedback 💬


Note

Medium Risk
New API surface and lifecycle operations (delete/prune) touch volumes and builder state; misconfiguration of idle_ttl or quotas could delete or block builders unexpectedly, but changes are scoped with tests and validation.

Overview
Adds a Builder HTTP API for persistent BuildKit cache disks: list (with tag filter), create with eager cache volume provisioning, get/delete by opaque ID, and prune to reset the cache asynchronously (202, status pruning).

BuilderManager is injected into ApiService, wired through config (builders.*), providers, and startup (Start for reconciliation/idle reaper). /builders/{id} routes use ResolveResource middleware with a new BuilderResolver and builder:read / builder:write / builder:delete scopes.

Handlers map domain errors to 400/409/404 (duplicate ID, quota, in-use, invalid ID/tags). OpenAPI and generated oapi client/server code include the new routes and Builder types. Regenerating oapi.go renamed ImageStatus enum constants (oapi.Ready etc.); one registry test was updated accordingly.

Reviewed by Cursor Bugbot for commit a6f1763. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

✱ Stainless preview builds for hypeman

This PR will update the hypeman SDKs with the following commit message.

feat: api: add /builders CRUD and prune endpoints

Edit this comment to update it. It will appear in the SDK's changelogs.

hypeman-openapi studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅

New diagnostics (5 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /builders`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /builders`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /builders/{id}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /builders/{id}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /builders/{id}/prune`
hypeman-typescript studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ⏭️ (prev: build ✅) → lint ⏭️ (prev: lint ❗) → test ✅

New diagnostics (5 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /builders`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /builders`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /builders/{id}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /builders/{id}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /builders/{id}/prune`
hypeman-go studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ⏭️ (prev: build ✅) → lint ✅test ✅

go get github.com/stainless-sdks/hypeman-go@4a392f64570694ff9d083272899049e39973c0ee
New diagnostics (5 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /builders`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /builders`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `get /builders/{id}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `delete /builders/{id}`
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /builders/{id}/prune`

This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-08-03 23:26:51 UTC

@rgarcia
rgarcia force-pushed the hypeship/builders-api branch from 8820a02 to ac772e4 Compare August 3, 2026 23:05
Expose the Builder resource over the API. POST /builders creates a
builder with an optional caller-supplied ID (validated, conflicts on
replay for control-plane idempotency), optional non-unique name, tags,
and disk size; the cache disk is provisioned eagerly and 201 returns the
builder. GET /builders lists with tag filtering and always returns a
non-nil array; GET /builders/{id} returns one builder; DELETE
/builders/{id} removes builder and disk (409 while in use); POST
/builders/{id}/prune resets the cache disk asynchronously and returns
202 with status pruning. Responses carry id, name, disk_size_gb, status,
tags, created_at, and last_used_at only. Every builder route documents
the 401 bearer-auth failure response.

Builders resolve by opaque ID through the resource resolver middleware;
builder:read/write/delete scopes gate the routes. The builders manager
is wired through config (builders.max_count, default_disk_size_gb,
max_disk_size_gb, idle_ttl), providers, and the generated injector, and
started with the server so startup reconciliation runs. Regenerating
oapi.go renamed the ImageStatus enum constants (oapi-codegen conflict
resolution with the new BuilderStatus enum); the one referencing test is
updated.

Handler tests cover create (defaults, custom ID, invalid ID, idempotent
replay), get, list with tag filtering, delete, prune, and 409s while a
build holds the builder. Config validation tests cover the new
builders.* settings.
@rgarcia
rgarcia force-pushed the hypeship/builders-api branch from ac772e4 to a6f1763 Compare August 3, 2026 23:24
@rgarcia
rgarcia marked this pull request as ready for review August 3, 2026 23:44

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Prune misses not-found race
    • PruneBuilder now maps ResetDisk returning ErrNotFound to a 404 not_found response, matching the documented race handling used by DeleteBuilder.

Create PR

Or push these changes by commenting:

@cursor push d305f20bd3
Preview (d305f20bd3)
diff --git a/cmd/api/api/builders.go b/cmd/api/api/builders.go
--- a/cmd/api/api/builders.go
+++ b/cmd/api/api/builders.go
@@ -149,6 +149,13 @@
 	log := logger.FromContext(ctx)
 
 	if err := s.BuilderManager.ResetDisk(ctx, b.ID); err != nil {
+		if errors.Is(err, builders.ErrNotFound) {
+			// Deleted between resolution and this call (e.g. idle reaper)
+			return oapi.PruneBuilder404JSONResponse{
+				Code:    "not_found",
+				Message: "builder not found",
+			}, nil
+		}
 		if errors.Is(err, builders.ErrInUse) {
 			return oapi.PruneBuilder409JSONResponse{
 				Code:    "conflict",

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit a6f1763. Configure here.

Comment thread cmd/api/api/builders.go
Code: "internal_error",
Message: "failed to prune builder",
}, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Prune misses not-found race

Medium Severity

PruneBuilder maps ErrInUse to 409 but not ErrNotFound. If the idle reaper removes the builder between middleware resolution and ResetDisk, the API returns 500 instead of the documented 404. DeleteBuilder already handles this race.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a6f1763. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant