Skip to content

Commit 1ada27f

Browse files
authored
Add thv skill builds command to list and remove locally-built OCI skill artifacts (#4674)
* Bump toolhive-core to v0.0.14 * Add DeleteBuild to SkillService interface and implementation * Regenerate SkillService mock for DeleteBuild * Add DELETE /api/v1beta/skills/builds/{tag} endpoint * Add DeleteBuild to skills HTTP client * Add thv skill builds remove command * Add unit tests for DeleteBuild service method * Regenerate CLI docs with skill builds remove command * Regenerate OpenAPI spec with DELETE /builds/{tag} endpoint
1 parent 56b9138 commit 1ada27f

15 files changed

Lines changed: 354 additions & 1 deletion

File tree

cmd/thv/app/skill_builds_remove.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package app
5+
6+
import (
7+
"fmt"
8+
9+
"github.com/spf13/cobra"
10+
)
11+
12+
var skillBuildsRemoveCmd = &cobra.Command{
13+
Use: "remove <tag>",
14+
Short: "Remove a locally-built skill artifact",
15+
Long: `Remove a locally-built OCI skill artifact and its blobs from the local OCI store.`,
16+
Args: cobra.ExactArgs(1),
17+
RunE: skillBuildsRemoveCmdFunc,
18+
}
19+
20+
func init() {
21+
skillBuildsCmd.AddCommand(skillBuildsRemoveCmd)
22+
}
23+
24+
func skillBuildsRemoveCmdFunc(cmd *cobra.Command, args []string) error {
25+
c := newSkillClient(cmd.Context())
26+
if err := c.DeleteBuild(cmd.Context(), args[0]); err != nil {
27+
return formatSkillError("remove build", err)
28+
}
29+
fmt.Printf("Removed build %q\n", args[0])
30+
return nil
31+
}

docs/cli/thv_skill_builds.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cli/thv_skill_builds_remove.md

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/docs.go

Lines changed: 52 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/swagger.json

Lines changed: 52 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/swagger.yaml

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ require (
4545
github.com/shirou/gopsutil/v4 v4.26.2
4646
github.com/spf13/viper v1.21.0
4747
github.com/stacklok/toolhive-catalog v0.20260406.0
48-
github.com/stacklok/toolhive-core v0.0.13
48+
github.com/stacklok/toolhive-core v0.0.14
4949
github.com/stretchr/testify v1.11.1
5050
github.com/swaggo/swag/v2 v2.0.0-rc5
5151
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,8 @@ github.com/stacklok/toolhive-catalog v0.20260406.0 h1:MzcSoYJmjwf+sOXiw9uw6wzfz/
802802
github.com/stacklok/toolhive-catalog v0.20260406.0/go.mod h1:VeHaVQx4dP484wxQT947GA+ocP4YiuKCTN+v7upEPiU=
803803
github.com/stacklok/toolhive-core v0.0.13 h1:iKMnI7VIVeBXkeCccF5UdOMy5RUPeL+Bmg4DjgFeiHU=
804804
github.com/stacklok/toolhive-core v0.0.13/go.mod h1:AAeOC8CxDVtburJEkVVdR2bO5z2fiY9dgYRnhcjkHvI=
805+
github.com/stacklok/toolhive-core v0.0.14 h1:/tyTrtoAMDPH66q1aeKIDDe50P4RGxKGP+bG+7MZ7gs=
806+
github.com/stacklok/toolhive-core v0.0.14/go.mod h1:MQ+SN7cUwoKj5TX/LmuY1WLgDBm2vRpRwwwYOlT3hug=
805807
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
806808
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
807809
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=

pkg/api/v1/skills.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func SkillsRouter(skillService skills.SkillService) http.Handler {
3535
r.Post("/build", apierrors.ErrorHandler(routes.buildSkill))
3636
r.Post("/push", apierrors.ErrorHandler(routes.pushSkill))
3737
r.Get("/builds", apierrors.ErrorHandler(routes.listBuilds))
38+
r.Delete("/builds/{tag}", apierrors.ErrorHandler(routes.deleteBuild))
3839

3940
return r
4041
}
@@ -302,3 +303,22 @@ func (s *SkillsRoutes) listBuilds(w http.ResponseWriter, r *http.Request) error
302303
w.Header().Set("Content-Type", "application/json")
303304
return json.NewEncoder(w).Encode(buildListResponse{Builds: builds})
304305
}
306+
307+
// deleteBuild removes a locally-built OCI skill artifact from the local store.
308+
//
309+
// @Summary Delete a locally-built skill artifact
310+
// @Description Remove a locally-built OCI skill artifact and its blobs from the local store
311+
// @Tags skills
312+
// @Param tag path string true "Artifact tag"
313+
// @Success 204 {string} string "No Content"
314+
// @Failure 404 {string} string "Not Found"
315+
// @Failure 500 {string} string "Internal Server Error"
316+
// @Router /api/v1beta/skills/builds/{tag} [delete]
317+
func (s *SkillsRoutes) deleteBuild(w http.ResponseWriter, r *http.Request) error {
318+
tag := chi.URLParam(r, "tag")
319+
if err := s.skillService.DeleteBuild(r.Context(), tag); err != nil {
320+
return err
321+
}
322+
w.WriteHeader(http.StatusNoContent)
323+
return nil
324+
}

pkg/api/v1/skills_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,36 @@ func TestSkillsRouter(t *testing.T) {
525525
expectedStatus: http.StatusInternalServerError,
526526
expectedBody: "Internal Server Error",
527527
},
528+
{
529+
name: "delete build success",
530+
method: "DELETE",
531+
path: "/builds/my-skill",
532+
setupMock: func(svc *skillsmocks.MockSkillService, _ string) {
533+
svc.EXPECT().DeleteBuild(gomock.Any(), "my-skill").Return(nil)
534+
},
535+
expectedStatus: http.StatusNoContent,
536+
},
537+
{
538+
name: "delete build not found",
539+
method: "DELETE",
540+
path: "/builds/missing",
541+
setupMock: func(svc *skillsmocks.MockSkillService, _ string) {
542+
svc.EXPECT().DeleteBuild(gomock.Any(), "missing").
543+
Return(httperr.WithCode(fmt.Errorf("tag not found"), http.StatusNotFound))
544+
},
545+
expectedStatus: http.StatusNotFound,
546+
},
547+
{
548+
name: "delete build service error",
549+
method: "DELETE",
550+
path: "/builds/my-skill",
551+
setupMock: func(svc *skillsmocks.MockSkillService, _ string) {
552+
svc.EXPECT().DeleteBuild(gomock.Any(), "my-skill").
553+
Return(httperr.WithCode(fmt.Errorf("oci store not configured"), http.StatusInternalServerError))
554+
},
555+
expectedStatus: http.StatusInternalServerError,
556+
expectedBody: "Internal Server Error",
557+
},
528558
}
529559

530560
for _, tt := range tests {

0 commit comments

Comments
 (0)