Skip to content

Commit 91ea982

Browse files
committed
Filter ListBuilds to skill artifacts by ArtifactType
1 parent 7c396cc commit 91ea982

2 files changed

Lines changed: 67 additions & 6 deletions

File tree

pkg/skills/skillsvc/skillsvc.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,15 @@ func (s *service) ListBuilds(ctx context.Context) ([]skills.LocalBuild, error) {
523523
continue
524524
}
525525

526+
isSkill, typeErr := s.isSkillArtifact(ctx, d)
527+
if typeErr != nil {
528+
slog.Debug("failed to check artifact type in local OCI store", "tag", tag, "error", typeErr)
529+
continue
530+
}
531+
if !isSkill {
532+
continue
533+
}
534+
526535
build := skills.LocalBuild{
527536
Tag: tag,
528537
Digest: d.String(),
@@ -986,6 +995,34 @@ func buildGitReferenceFromRegistryURL(rawURL string) (string, error) {
986995
return gitURL, nil
987996
}
988997

998+
// isSkillArtifact reports whether the OCI descriptor at digest d carries
999+
// ArtifactType == ArtifactTypeSkill. It inspects the top-level index or
1000+
// manifest without descending into layers, so it is cheap to call.
1001+
func (s *service) isSkillArtifact(ctx context.Context, d digest.Digest) (bool, error) {
1002+
isIndex, err := s.ociStore.IsIndex(ctx, d)
1003+
if err != nil {
1004+
return false, fmt.Errorf("checking OCI content type: %w", err)
1005+
}
1006+
1007+
if isIndex {
1008+
index, indexErr := s.ociStore.GetIndex(ctx, d)
1009+
if indexErr != nil {
1010+
return false, fmt.Errorf("reading OCI index: %w", indexErr)
1011+
}
1012+
return index.ArtifactType == ociskills.ArtifactTypeSkill, nil
1013+
}
1014+
1015+
manifestBytes, err := s.ociStore.GetManifest(ctx, d)
1016+
if err != nil {
1017+
return false, fmt.Errorf("reading OCI manifest: %w", err)
1018+
}
1019+
var manifest ocispec.Manifest
1020+
if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
1021+
return false, fmt.Errorf("parsing OCI manifest: %w", err)
1022+
}
1023+
return manifest.ArtifactType == ociskills.ArtifactTypeSkill, nil
1024+
}
1025+
9891026
// extractOCIContent navigates the OCI content graph from a pulled digest,
9901027
// extracting the skill config and raw layer data.
9911028
func (s *service) extractOCIContent(ctx context.Context, d digest.Digest) ([]byte, *ociskills.SkillConfig, error) {

pkg/skills/skillsvc/skillsvc_test.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2812,25 +2812,49 @@ func TestListBuilds(t *testing.T) {
28122812
assert.Equal(t, "2.0.0", names["skill-b"])
28132813
})
28142814

2815-
t.Run("artifact with no metadata still appears", func(t *testing.T) {
2815+
t.Run("skill artifact with no extractable metadata still appears", func(t *testing.T) {
28162816
t.Parallel()
28172817
ociStore, err := ociskills.NewStore(t.TempDir())
28182818
require.NoError(t, err)
28192819

2820-
// Store a minimal manifest with no config labels — extractOCIContent will fail
2821-
// but the artifact should still appear with empty metadata fields.
2822-
d, putErr := ociStore.PutManifest(t.Context(), []byte(`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[]}`))
2820+
// Store an index with ArtifactType set to the skill type but no child manifests —
2821+
// extractOCIContent will fail but the artifact should still appear with empty metadata fields.
2822+
skillIndex := `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","artifactType":"dev.toolhive.skills.v1","manifests":[]}`
2823+
d, putErr := ociStore.PutManifest(t.Context(), []byte(skillIndex))
28232824
require.NoError(t, putErr)
2824-
require.NoError(t, ociStore.Tag(t.Context(), d, "bare-tag"))
2825+
require.NoError(t, ociStore.Tag(t.Context(), d, "bare-skill-tag"))
28252826

28262827
svc := New(&storage.NoopSkillStore{}, WithOCIStore(ociStore))
28272828
artifacts, err := svc.ListBuilds(t.Context())
28282829
require.NoError(t, err)
28292830
require.Len(t, artifacts, 1)
28302831

2831-
assert.Equal(t, "bare-tag", artifacts[0].Tag)
2832+
assert.Equal(t, "bare-skill-tag", artifacts[0].Tag)
28322833
assert.Contains(t, artifacts[0].Digest, "sha256:")
28332834
assert.Empty(t, artifacts[0].Name)
28342835
assert.Empty(t, artifacts[0].Version)
28352836
})
2837+
2838+
t.Run("non-skill artifact is excluded", func(t *testing.T) {
2839+
t.Parallel()
2840+
ociStore, err := ociskills.NewStore(t.TempDir())
2841+
require.NoError(t, err)
2842+
2843+
// Store a valid skill artifact that should be returned.
2844+
skillDigest := buildTestArtifact(t, ociStore, "real-skill", "1.0.0")
2845+
require.NoError(t, ociStore.Tag(t.Context(), skillDigest, "real-skill"))
2846+
2847+
// Store an index whose ArtifactType is not the skill type — simulates a
2848+
// remotely-pulled non-skill OCI artifact sharing the same store.
2849+
otherIndex := `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","artifactType":"application/vnd.docker.distribution.manifest.v2","manifests":[]}`
2850+
otherDigest, putErr := ociStore.PutManifest(t.Context(), []byte(otherIndex))
2851+
require.NoError(t, putErr)
2852+
require.NoError(t, ociStore.Tag(t.Context(), otherDigest, "non-skill-tag"))
2853+
2854+
svc := New(&storage.NoopSkillStore{}, WithOCIStore(ociStore))
2855+
artifacts, err := svc.ListBuilds(t.Context())
2856+
require.NoError(t, err)
2857+
require.Len(t, artifacts, 1)
2858+
assert.Equal(t, "real-skill", artifacts[0].Tag)
2859+
})
28362860
}

0 commit comments

Comments
 (0)