feat(marketplace): record and pin a digest in community index entries

An index entry can now carry a digest (sha256:<64 hex>) alongside its version.
Resolving a short name pins that digest, so 'cozypkg tap <name>' consumes the
exact content the CI gate verified rather than following a mutable tag; the
version is kept informational. A malformed digest in an entry is a hard load
error, consistent with the rest of the strict index decoding.

Assisted-by: LLM
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
This commit is contained in:
IvanHunters committed 2026-10-22 22:16:39 +03:00
1 parent c56b9876f4
commit c59ced04cc
2 files changed
+42 -5

No files matched your search

+17 -5
View File
@@ -38,7 +38,11 @@ type IndexEntry struct {
// OCIRef is the artifact location, e.g. oci://ghcr.io/org/repo.
OCIRef string `json:"ociRef"`
// Version is the OCI tag of the listed release; a version bump updates it.
Version string `json:"version,omitempty"`
Version string `json:"version,omitempty"`
// Digest pins the exact content the CI gate verified (sha256:<64 hex>). When
// set, tapping the entry's short name pins this digest, so a short-name tap
// does not follow a moved tag. The version is kept informational alongside it.
Digest string `json:"digest,omitempty"`
Description string `json:"description,omitempty"`
Homepage string `json:"homepage,omitempty"`
Maintainer string `json:"maintainer,omitempty"`
@@ -88,6 +92,9 @@ func loadIndex(dir string) ([]IndexEntry, error) {
if entry.Name == "" || entry.OCIRef == "" {
return fmt.Errorf("%s: index entry must set both name and ociRef", rel)
}
if entry.Digest != "" && !ociDigestRe.MatchString(entry.Digest) {
return fmt.Errorf("%s: index entry digest %q must be sha256:<64 hex>", rel, entry.Digest)
}
entries = append(entries, entry)
return nil
})
@@ -218,13 +225,18 @@ func resolveTapTarget(arg, indexRef string) (string, error) {
if !ok {
return "", fmt.Errorf("no index entry named %q", arg)
}
// Pin to the entry's recorded version: the CI gate validated and
// Pin to the entry's recorded version and digest: the CI gate validated and
// cosign-verified <ociRef>:<version>, so tapping must consume that exact
// tag rather than defaulting to latest.
// release rather than defaulting to latest. A recorded digest pins the exact
// content (the strongest form); the version is kept informational alongside it.
ref := entry.OCIRef
if entry.Version != "" {
return entry.OCIRef + ":" + entry.Version, nil
ref += ":" + entry.Version
}
return entry.OCIRef, nil
if entry.Digest != "" {
ref += "@" + entry.Digest
}
return ref, nil
}
func init() {
+25
View File
@@ -124,6 +124,31 @@ func TestResolveTapTarget(t *testing.T) {
}
}
func TestResolveTapTargetPinsDigest(t *testing.T) {
const digest = "sha256:1111111111111111111111111111111111111111111111111111111111111111"
dir := t.TempDir()
writeFile(t, dir, "pinned.yaml", "name: pin.ned\nociRef: oci://ghcr.io/foo/bar\nversion: v1.2.3\ndigest: "+digest+"\n")
writeFile(t, dir, "digestonly.yaml", "name: dig.only\nociRef: oci://ghcr.io/baz/qux\ndigest: "+digest+"\n")
// A recorded digest pins the exact content the CI gate verified; the version
// is kept informational alongside it.
if got, err := resolveTapTarget("pin.ned", dir); err != nil || got != "oci://ghcr.io/foo/bar:v1.2.3@"+digest {
t.Fatalf("version+digest resolve = %q err %v", got, err)
}
// A digest with no version resolves to the bare ref pinned by digest.
if got, err := resolveTapTarget("dig.only", dir); err != nil || got != "oci://ghcr.io/baz/qux@"+digest {
t.Fatalf("digest-only resolve = %q err %v", got, err)
}
}
func TestLoadIndexRejectsBadDigest(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, "bad.yaml", "name: x\nociRef: oci://r/x\ndigest: sha256:not-a-digest\n")
if _, err := loadIndex(dir); err == nil {
t.Fatal("expected error for a malformed digest in an index entry")
}
}
func TestDefaultIndexRef(t *testing.T) {
if got := defaultIndexRef("/explicit"); got != "/explicit" {
t.Errorf("flag should win, got %q", got)