carlos / platform Public

release: tags — propagate ListTags GetTag errors beyond list/delete races

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 17bbf9205f738dd928fa67088bf6a7677de381f6 parent d9c098d
2 files changed, +32 −3
  • internal/release/tag.go +9 −3
  • internal/release/tag_test.go +23 −0
diff --git a/internal/release/tag.go b/internal/release/tag.go
index c897cb366..f00e8b909 100644
--- a/internal/release/tag.go
+++ b/internal/release/tag.go
@@ -11,7 +11,9 @@ package release
import (
"context"
"encoding/json"
+ "errors"
"fmt"
+ "os"
"sort"
"strings"
"time"
@@ -53,8 +55,9 @@ func GetTag(ctx context.Context, s Store, acct, app, version string) (*Tag, erro
}
// ListTags lists an app's tags, semver-descending — newest line first,
-// which string order gets wrong the day 0.10.0 exists. A key that does not
-// parse as a semver tag is skipped, not fatal.
+// which string order gets wrong the day 0.10.0 exists. Skips key names
+// that don't parse as semver and os.ErrNotExist misses (list/delete race).
+// Returns other GetTag errors wrapped with the key.
func ListTags(ctx context.Context, s Store, acct, app string) ([]Tag, error) {
prefix := TagsPrefix(acct, app)
keys, err := s.List(ctx, prefix)
@@ -69,7 +72,10 @@ func ListTags(ctx context.Context, s Store, acct, app string) ([]Tag, error) {
}
t, err := GetTag(ctx, s, acct, app, name)
if err != nil {
- continue
+ if errors.Is(err, os.ErrNotExist) {
+ continue
+ }
+ return nil, fmt.Errorf("tag at %s: %w", k, err)
}
out = append(out, *t)
}
diff --git a/internal/release/tag_test.go b/internal/release/tag_test.go
index a207f3485..91462ef11 100644
--- a/internal/release/tag_test.go
+++ b/internal/release/tag_test.go
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"os"
+ "strings"
"testing"
"time"
)
@@ -58,3 +59,25 @@ func TestTagImmutable(t *testing.T) {
t.Fatal("conflicting rewrite accepted")
}
}
+
+func TestTagListErrors(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ // Write a valid tag and a garbage tag (malformed JSON).
+ good := Tag{Schema: 1, Version: "0.5.0", Release: "0.5.0-1", TaggedAt: time.Now().UTC()}
+ if err := writeTag(ctx, s, "acct1", "km", &good); err != nil {
+ t.Fatal(err)
+ }
+ // Write garbage bytes directly to a valid tag key.
+ if _, err := s.Put(ctx, TagKey("acct1", "km", "0.7.0"), []byte("not json"), ""); err != nil {
+ t.Fatal(err)
+ }
+ // ListTags should return an error naming the key, not silently skip it.
+ _, err := ListTags(ctx, s, "acct1", "km")
+ if err == nil {
+ t.Fatal("ListTags accepted malformed JSON")
+ }
+ if !strings.Contains(err.Error(), "0.7.0") {
+ t.Fatalf("error does not name the key: %v", err)
+ }
+}