Install local skill builds by name when tag differs#5182
Open
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5182 +/- ##
=======================================
Coverage 67.72% 67.72%
=======================================
Files 607 607
Lines 61984 62063 +79
=======================================
+ Hits 41978 42033 +55
- Misses 16845 16859 +14
- Partials 3161 3171 +10 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Pull the LayerData/Digest/Reference/Version assignment block out of resolveFromLocalStore into a small hydrateOptsFromLocalBuild helper so a follow-up can share it between the existing direct-tag path and a new local-build name-scan path. No behavior change.
A local skill build tagged with something other than its declared skill name (e.g. thv skill build --tag v0.0.1) was not findable via POST /api/v1beta/skills with a plain name + version body: install-by-name did a literal tag lookup against the local OCI store, so the only resolvable handle was the OCI tag string. Callers got 404 "not found in local store or registry" even though the build was visible in GET /skills/builds. Extend resolveFromLocalStore so that when the direct tag lookup misses, or when a caller-supplied version disagrees with the direct match, it walks the local-build-marked tags for one whose declared skill name matches. The version filter accepts either the artifact's cfg.Version or the local store tag, mirroring the OCI-name install path that already treats version as the tag and matching the GET /skills/builds output where many artifacts carry a tag like "v0.0.1" with an empty SKILL.md version. With multiple matches, prefer the build whose tag equals the skill name; otherwise return 409 Conflict listing each candidate's tag and version. Add table-driven cases covering: scan resolves by skill name when tag differs, version matches the local store tag (the user-facing reproducer), version filter narrows multiple matches, version mismatch falls through to registry, direct version mismatch falls through to scan, ambiguous matches return 409, and non-skill local-build-marked tags are ignored. Tighten the test runner so wantErr is also asserted when wantCode is set.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
POST /api/v1beta/skillswith{"name":"<skill-name>","scope":"user","version":"<tag-or-version>"}returned404 Not Found("not found in local store or registry; install by OCI reference: …") when the only matching artifact was a local build tagged with something other than the skill name — e.g. a build produced bythv skill build --tag v0.0.1. The build was clearly visible inGET /api/v1beta/skills/builds(which exposesname,tag,version,digest), but install-by-name only resolved a literal tag in the local OCI store, so the skill name was effectively unbindable to that build.versionfield in the request was also being interpreted strictly ascfg.Version(the artifact's SKILL.md frontmatter version). Many local builds have an emptycfg.Versionwhile carrying a meaningful OCI tag, and the existing OCI-name install path already treatsversionas the tag — so a caller passing the tag string fromGET /skills/buildsasversionwas silently filtered out.resolveFromLocalStorenow falls back to scanning local-build-marked tags by declared skill name (and version, when specified) when the direct tag lookup misses or its version disagrees with the request. The version filter matches against eithercfg.Versionor the local store tag. Multiple matches resolve via a tag-equals-name tie-breaker; otherwise the API returns409 Conflictwith the candidate list.Resolution flow after the change
flowchart TD in[POST /skills name + optional version] --> direct{Direct tag lookup of name} direct -->|"found, name matches, version ok"| useDirect[Use direct tag] direct -->|"found, name mismatches"| err422[422 supply-chain - existing] direct -->|"found, version mismatches cfg"| scan[Scan local-build tags] direct -->|not found| scan scan --> filter{"Filter: cfg.Name == name AND (version empty OR cfg.Version == version OR tag == version)"} filter -->|0 matches| reg[Fall through to registry - existing] filter -->|1 match| useScan[Use that match] filter -->|multiple matches| tie{Any tag equals name} tie -->|yes| useTagged[Use tag-named build] tie -->|no| ambig[409 Conflict - list candidates]opts.Namein the local OCI store (existing fast path):422 Unprocessable Entity(existing supply-chain check).versiondisagrees withcfg.Version→ fall through to scan.cfg.Name == opts.NameAND (opts.Versionempty ORcfg.Version == opts.VersionORtag == opts.Version):404/registry-resolved paths).409 Conflictlisting each candidate's(tag, version)so callers can disambiguate by passingversion.The scan is gated on the
dev.stacklok.toolhive.local-builddescriptor annotation, so only artifacts produced bythv skill buildare considered — pulled artifacts cached in the local OCI store stay invisible, matchingListBuilds.Referenceon the resultingInstalledSkilldefaults to the resolved local store tag (e.g.v0.0.1) rather than the skill name, so a laterthv skill push --reference <tag>can re-resolve the artifact.Type of change
versionaccepts either the artifact's declared version or the local store tag)Test plan
task test)task lint-fix— 0 issues)TestInstallFromLocalStorecases assert the new behavior end-to-end viasvc.Install:scan resolves by skill name when tag differs— the original "not found" reproducer.scan version matches local store tag— the second reproducer:cfg.Versiondiffers from the displayed tag, caller passes the tag asversion.scan version filter selects matching build— two builds, same name, differentcfg.Versions; matching version wins.scan ambiguous matches return 409 with candidate list— two builds with same name+version, no tag-equals-name winner.scan version mismatch falls through to registry— name matches but neithercfg.Versionnor tag matches; falls through to the existing registry-lookup404.direct version mismatch falls through to scan— direct tag matches name but not version; scan locates the sibling build with the right version.scan ignores non-skill local-build-marked tags— defensive: a local-build-marked entry whoseartifactTypeis not the skill type is skipped.wantErris asserted alongsidewantCode; this also tightens the existingname mismatch in local artifactandcorrupt manifest propagates errorcases (no behavior change, just stricter assertions).thv serve:POST /api/v1beta/skills {"name":"<skill-name>","scope":"user","version":"v0.0.1"}against a local store containing only tagv0.0.1(with emptycfg.Version) — installs successfully.Changes
pkg/skills/skillsvc/install_oci.gohydrateOptsFromLocalBuild. SplitresolveFromLocalStoreintotryDirectLocalTag+tryLocalBuildScan. AddlocalBuildMatch,findLocalBuildsByName,pickLocalBuildMatch,ambiguousLocalBuildError. Direct path now falls through to scan when a caller-supplied version disagrees. Scan version filter accepts eithercfg.Versionor the local store tag. New imports:github.com/opencontainers/go-digest,ociskills "github.com/stacklok/toolhive-core/oci/skills".pkg/skills/skillsvc/install_oci_test.goTestInstallFromLocalStorecases; runner assertswantErrwhenwantCodeis set.Does this introduce a user-facing change?
Yes — bug-fix only.
version) onPOST /api/v1beta/skillsnow resolve to a local build whose declared skill name matches, regardless of the OCI tag the build was produced under. This unblocks the natural workflow ofthv skill build --tag <version>followed by install-by-name from the studio UI / API.versionfield is now matched against either the artifact's declared version (cfg.Version) or the local store tag, matching whatGET /skills/buildsdisplays and the existing OCI-name install behavior.409 Conflictresponse when the local store has multiple builds for the same skill name and the call did not supply aversionto disambiguate (the response body lists each candidate's tag and version).422) are unchanged.Commit commands are unchanged from the previous turn. Want me to run them?