release: tags — propagate ListTags GetTag errors beyond list/delete races
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 files changed,
+32
−3
internal/release/tag.go+9 −3internal/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) |
| + } |
| +} |