detached c57f9ec

feat(releases): release domain, kv storage, ports and schema 6 with release:write

commit
c57f9ecfd8cf0f9e6a87bf9184ede051bf11fd9f
author
brice · · 2026-10-21 14:19 UTC
committer
brice · · 2026-10-21 17:53 UTC
parent
f61e6a6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
19 files · +3685 -15
A gen/ermite/storage/release.pb.go +463 -0 generated file: collapsed
M internal/adapters/kvstore/issues_test.go +16 -8
@@ -271,7 +271,8 @@ func TestRepoSettingsKeepTheIssuesMode(t *testing.T) {
271271
272272 // TestSchema4UpgradesTheTokensWithAllScopes: the tokens that held every
273273 // original scope keep their owner rights, gaining mr:review and issue:write
274-// (each only if missing), then mr:force-merge in schema 5; no other token changes.
274+// (each only if missing), then mr:force-merge in schema 5 and release:write in
275+// schema 6; no other token changes.
275276 func TestSchema4UpgradesTheTokensWithAllScopes(t *testing.T) {
276277 ctx := context.Background()
277278 dir := t.TempDir()
@@ -309,9 +310,14 @@ func TestSchema4UpgradesTheTokensWithAllScopes(t *testing.T) {
309310 full := get("full")
310311 assert.Contains(t, full.Scopes, domain.ScopeIssueWrite)
311312 assert.Contains(t, full.Scopes, domain.ScopeMRReview)
312- assert.Len(t, full.Scopes, 12)
313+ assert.Len(t, full.Scopes, 13)
314+ assert.Contains(t, full.Scopes, domain.ScopeReleaseWrite)
313315 full10 := get("full10")
314- assert.Len(t, full10.Scopes, 12)
316+ assert.Len(t, full10.Scopes, 13)
317+ assert.Contains(t, full10.Scopes, domain.ScopeReleaseWrite)
318+ for _, id := range []string{"repos", "prefix", "missing", "reposmr", "small"} {
319+ assert.NotContains(t, get(id).Scopes, domain.ScopeReleaseWrite)
320+ }
315321 assert.Equal(t, 1, countScope(full10.Scopes, domain.ScopeMRReview))
316322 assert.Contains(t, full10.Scopes, domain.ScopeIssueWrite)
317323 assert.True(t, domain.TokenPrincipal(full10).HasOwnerRights())
@@ -338,7 +344,7 @@ func TestSchema4UpgradesTheTokensWithAllScopes(t *testing.T) {
338344 assert.Equal(t, "full", full.ID)
339345
340346 require.NoError(t, s.Close(ctx))
341- assert.Equal(t, uint32(5), readSchema(t, dir))
347+ assert.Equal(t, uint32(kvstore.SchemaVersion), readSchema(t, dir))
342348
343349 // A second open changes nothing.
344350 s2, err := kvstore.Open(ctx, dir)
@@ -358,7 +364,7 @@ func TestSchemaNewerThanThisBuildStopsIssuesData(t *testing.T) {
358364 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: kvstore.SchemaVersion + 1}))
359365 _, err := kvstore.Open(ctx, dir)
360366 require.ErrorIs(t, err, kvstore.ErrSchemaTooNew)
361- assert.Equal(t, uint32(5), uint32(kvstore.SchemaVersion))
367+ assert.Equal(t, uint32(6), uint32(kvstore.SchemaVersion))
362368 }
363369
364370 // A reopen racing the delete of the repository either lands before the delete
@@ -571,7 +577,7 @@ func TestSchema5UpgradesOwnerEquivalentTokens(t *testing.T) {
571577 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: 4}))
572578 var previous []string
573579 for _, sc := range domain.AllScopes() {
574- if sc != domain.ScopeMRForceMerge {
580+ if sc != domain.ScopeMRForceMerge && sc != domain.ScopeReleaseWrite {
575581 previous = append(previous, sc.String())
576582 }
577583 }
@@ -594,9 +600,11 @@ func TestSchema5UpgradesOwnerEquivalentTokens(t *testing.T) {
594600 if id == "owner" {
595601 assert.True(t, domain.TokenPrincipal(tok).HasOwnerRights())
596602 assert.Equal(t, 1, countScope(tok.Scopes, domain.ScopeMRForceMerge))
597- assert.Len(t, tok.Scopes, 12)
603+ assert.Equal(t, 1, countScope(tok.Scopes, domain.ScopeReleaseWrite))
604+ assert.Len(t, tok.Scopes, 13)
598605 } else {
599606 assert.NotContains(t, tok.Scopes, domain.ScopeMRForceMerge)
607+ assert.NotContains(t, tok.Scopes, domain.ScopeReleaseWrite)
600608 var got []string
601609 for _, sc := range tok.Scopes {
602610 got = append(got, sc.String())
@@ -605,6 +613,6 @@ func TestSchema5UpgradesOwnerEquivalentTokens(t *testing.T) {
605613 }
606614 }
607615 require.NoError(t, s.Close(ctx))
608- assert.Equal(t, uint32(5), readSchema(t, dir))
616+ assert.Equal(t, uint32(kvstore.SchemaVersion), readSchema(t, dir))
609617 }
610618 }
@@ -271,7 +271,8 @@ func TestRepoSettingsKeepTheIssuesMode(t *testing.T) {
271 271
272 // TestSchema4UpgradesTheTokensWithAllScopes: the tokens that held every272 // TestSchema4UpgradesTheTokensWithAllScopes: the tokens that held every
273 // original scope keep their owner rights, gaining mr:review and issue:write273 // original scope keep their owner rights, gaining mr:review and issue:write
274-// (each only if missing), then mr:force-merge in schema 5; no other token changes.274+// (each only if missing), then mr:force-merge in schema 5 and release:write in
275+// schema 6; no other token changes.
275 func TestSchema4UpgradesTheTokensWithAllScopes(t *testing.T) {276 func TestSchema4UpgradesTheTokensWithAllScopes(t *testing.T) {
276 ctx := context.Background()277 ctx := context.Background()
277 dir := t.TempDir()278 dir := t.TempDir()
@@ -309,9 +310,14 @@ func TestSchema4UpgradesTheTokensWithAllScopes(t *testing.T) {
309 full := get("full")310 full := get("full")
310 assert.Contains(t, full.Scopes, domain.ScopeIssueWrite)311 assert.Contains(t, full.Scopes, domain.ScopeIssueWrite)
311 assert.Contains(t, full.Scopes, domain.ScopeMRReview)312 assert.Contains(t, full.Scopes, domain.ScopeMRReview)
312- assert.Len(t, full.Scopes, 12)313+ assert.Len(t, full.Scopes, 13)
314+ assert.Contains(t, full.Scopes, domain.ScopeReleaseWrite)
313 full10 := get("full10")315 full10 := get("full10")
314- assert.Len(t, full10.Scopes, 12)316+ assert.Len(t, full10.Scopes, 13)
317+ assert.Contains(t, full10.Scopes, domain.ScopeReleaseWrite)
318+ for _, id := range []string{"repos", "prefix", "missing", "reposmr", "small"} {
319+ assert.NotContains(t, get(id).Scopes, domain.ScopeReleaseWrite)
320+ }
315 assert.Equal(t, 1, countScope(full10.Scopes, domain.ScopeMRReview))321 assert.Equal(t, 1, countScope(full10.Scopes, domain.ScopeMRReview))
316 assert.Contains(t, full10.Scopes, domain.ScopeIssueWrite)322 assert.Contains(t, full10.Scopes, domain.ScopeIssueWrite)
317 assert.True(t, domain.TokenPrincipal(full10).HasOwnerRights())323 assert.True(t, domain.TokenPrincipal(full10).HasOwnerRights())
@@ -338,7 +344,7 @@ func TestSchema4UpgradesTheTokensWithAllScopes(t *testing.T) {
338 assert.Equal(t, "full", full.ID)344 assert.Equal(t, "full", full.ID)
339 345
340 require.NoError(t, s.Close(ctx))346 require.NoError(t, s.Close(ctx))
341- assert.Equal(t, uint32(5), readSchema(t, dir))347+ assert.Equal(t, uint32(kvstore.SchemaVersion), readSchema(t, dir))
342 348
343 // A second open changes nothing.349 // A second open changes nothing.
344 s2, err := kvstore.Open(ctx, dir)350 s2, err := kvstore.Open(ctx, dir)
@@ -358,7 +364,7 @@ func TestSchemaNewerThanThisBuildStopsIssuesData(t *testing.T) {
358 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: kvstore.SchemaVersion + 1}))364 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: kvstore.SchemaVersion + 1}))
359 _, err := kvstore.Open(ctx, dir)365 _, err := kvstore.Open(ctx, dir)
360 require.ErrorIs(t, err, kvstore.ErrSchemaTooNew)366 require.ErrorIs(t, err, kvstore.ErrSchemaTooNew)
361- assert.Equal(t, uint32(5), uint32(kvstore.SchemaVersion))367+ assert.Equal(t, uint32(6), uint32(kvstore.SchemaVersion))
362 }368 }
363 369
364 // A reopen racing the delete of the repository either lands before the delete370 // A reopen racing the delete of the repository either lands before the delete
@@ -571,7 +577,7 @@ func TestSchema5UpgradesOwnerEquivalentTokens(t *testing.T) {
571 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: 4}))577 putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: 4}))
572 var previous []string578 var previous []string
573 for _, sc := range domain.AllScopes() {579 for _, sc := range domain.AllScopes() {
574- if sc != domain.ScopeMRForceMerge {580+ if sc != domain.ScopeMRForceMerge && sc != domain.ScopeReleaseWrite {
575 previous = append(previous, sc.String())581 previous = append(previous, sc.String())
576 }582 }
577 }583 }
@@ -594,9 +600,11 @@ func TestSchema5UpgradesOwnerEquivalentTokens(t *testing.T) {
594 if id == "owner" {600 if id == "owner" {
595 assert.True(t, domain.TokenPrincipal(tok).HasOwnerRights())601 assert.True(t, domain.TokenPrincipal(tok).HasOwnerRights())
596 assert.Equal(t, 1, countScope(tok.Scopes, domain.ScopeMRForceMerge))602 assert.Equal(t, 1, countScope(tok.Scopes, domain.ScopeMRForceMerge))
597- assert.Len(t, tok.Scopes, 12)603+ assert.Equal(t, 1, countScope(tok.Scopes, domain.ScopeReleaseWrite))
604+ assert.Len(t, tok.Scopes, 13)
598 } else {605 } else {
599 assert.NotContains(t, tok.Scopes, domain.ScopeMRForceMerge)606 assert.NotContains(t, tok.Scopes, domain.ScopeMRForceMerge)
607+ assert.NotContains(t, tok.Scopes, domain.ScopeReleaseWrite)
600 var got []string608 var got []string
601 for _, sc := range tok.Scopes {609 for _, sc := range tok.Scopes {
602 got = append(got, sc.String())610 got = append(got, sc.String())
@@ -605,6 +613,6 @@ func TestSchema5UpgradesOwnerEquivalentTokens(t *testing.T) {
605 }613 }
606 }614 }
607 require.NoError(t, s.Close(ctx))615 require.NoError(t, s.Close(ctx))
608- assert.Equal(t, uint32(5), readSchema(t, dir))616+ assert.Equal(t, uint32(kvstore.SchemaVersion), readSchema(t, dir))
609 }617 }
610 }618 }
A internal/adapters/kvstore/releases.go +408 -0
@@ -0,0 +1,408 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+package kvstore
3+
4+import (
5+ kv "bnema.dev/ermite-kv"
6+ storagepb "bnema.dev/ermite/gen/ermite/storage"
7+ "bnema.dev/ermite/internal/domain"
8+ "bnema.dev/ermite/internal/ports"
9+ "context"
10+ "errors"
11+ "fmt"
12+ "github.com/bnema/zerowrap"
13+ "google.golang.org/protobuf/proto"
14+ "slices"
15+ "strconv"
16+)
17+
18+// Releases implements ports.ReleaseStore over the shared metadata database.
19+type Releases struct{ db *kv.DB }
20+
21+var _ ports.ReleaseStore = (*Releases)(nil)
22+
23+// Releases returns the release metadata store.
24+func (s *Store) Releases() *Releases { return &Releases{db: s.db} }
25+func relKey(repo domain.RepoName, id int64) []byte {
26+ return []byte("rel/" + repo.String() + "/" + fmt.Sprintf(idFormat, id))
27+}
28+func relIDKey(id int64) []byte { return []byte("relid/" + fmt.Sprintf(idFormat, id)) }
29+func relTagKey(repo domain.RepoName, tag string) []byte {
30+ return []byte("reltag/" + repo.String() + "/" + tag)
31+}
32+func relAssetKey(id int64) []byte { return []byte("relasset/" + fmt.Sprintf(idFormat, id)) }
33+func relNamePrefix(repo domain.RepoName, id int64) string {
34+ return "relassets/" + repo.String() + "/" + fmt.Sprintf(idFormat, id) + "/"
35+}
36+func relNameKey(a domain.ReleaseAsset) []byte {
37+ return []byte(relNamePrefix(a.Repo, a.ReleaseID) + a.Name)
38+}
39+func relUseKey(repo domain.RepoName) []byte { return []byte("reluse/" + repo.String()) }
40+func encodeRelease(r domain.Release) []byte {
41+ return mustMarshal(&storagepb.Release{
42+ Id: r.ID,
43+ Repo: r.Repo.String(),
44+ Tag: r.Tag,
45+ Title: r.Title,
46+ Notes: r.Notes,
47+ Draft: r.Draft,
48+ Prerelease: r.Prerelease,
49+ MakeLatest: r.MakeLatest,
50+ TargetCommitish: r.TargetCommitish,
51+ Author: r.Author,
52+ CreatedAt: encodeTime(r.CreatedAt),
53+ PublishedAt: encodeTime(r.PublishedAt),
54+ UpdatedAt: encodeTime(r.UpdatedAt),
55+ })
56+}
57+func decodeRelease(item kv.Item) (domain.Release, error) {
58+ var pb storagepb.Release
59+ if err := proto.Unmarshal(item.Value, &pb); err != nil {
60+ return domain.Release{}, err
61+ }
62+ repo, err := domain.ParseRepoName(pb.GetRepo())
63+ if err != nil {
64+ return domain.Release{}, err
65+ }
66+ return domain.Release{Version: item.Version,
67+ ID: pb.GetId(),
68+ Repo: repo,
69+ Tag: pb.GetTag(),
70+ Title: pb.GetTitle(),
71+ Notes: pb.GetNotes(),
72+ Draft: pb.GetDraft(),
73+ Prerelease: pb.GetPrerelease(),
74+ MakeLatest: pb.GetMakeLatest(),
75+ TargetCommitish: pb.GetTargetCommitish(),
76+ Author: pb.GetAuthor(),
77+ CreatedAt: decodeTime(pb.GetCreatedAt()),
78+ PublishedAt: decodeTime(pb.GetPublishedAt()),
79+ UpdatedAt: decodeTime(pb.GetUpdatedAt()),
80+ }, nil
81+}
82+func encodeReleaseAsset(r domain.ReleaseAsset) []byte {
83+ return mustMarshal(&storagepb.ReleaseAsset{
84+ Id: r.ID,
85+ ReleaseId: r.ReleaseID,
86+ Repo: r.Repo.String(),
87+ Name: r.Name,
88+ Label: r.Label,
89+ ContentType: r.ContentType,
90+ Size: r.Size,
91+ Digest: r.Digest,
92+ State: string(r.State),
93+ Uploader: r.Uploader,
94+ Charged: r.Charged,
95+ MultipartId: r.MultipartID,
96+ CreatedAt: encodeTime(r.CreatedAt),
97+ UpdatedAt: encodeTime(r.UpdatedAt),
98+ })
99+}
100+func decodeReleaseAsset(item kv.Item) (domain.ReleaseAsset, error) {
101+ var pb storagepb.ReleaseAsset
102+ if err := proto.Unmarshal(item.Value, &pb); err != nil {
103+ return domain.ReleaseAsset{}, err
104+ }
105+ repo, err := domain.ParseRepoName(pb.GetRepo())
106+ if err != nil {
107+ return domain.ReleaseAsset{}, err
108+ }
109+ return domain.ReleaseAsset{Version: item.Version,
110+ ID: pb.GetId(),
111+ ReleaseID: pb.GetReleaseId(),
112+ Repo: repo,
113+ Name: pb.GetName(),
114+ Label: pb.GetLabel(),
115+ ContentType: pb.GetContentType(),
116+ Size: pb.GetSize(),
117+ Digest: pb.GetDigest(),
118+ State: domain.ReleaseAssetState(pb.GetState()),
119+ Uploader: pb.GetUploader(),
120+ Charged: pb.GetCharged(),
121+ MultipartID: pb.GetMultipartId(),
122+ CreatedAt: decodeTime(pb.GetCreatedAt()),
123+ UpdatedAt: decodeTime(pb.GetUpdatedAt()),
124+ }, nil
125+}
126+
127+// NextReleaseID allocates a global monotonically increasing id.
128+func (c *Releases) NextReleaseID(ctx context.Context) (int64, error) {
129+ for range artifactIDTries {
130+ last, item, ok, err := (&CI{db: c.db}).readSeq(ctx, []byte("relseq"))
131+ if err != nil {
132+ return 0, err
133+ }
134+ var b kv.Batch
135+ stageSeq(&b, []byte("relseq"), item, ok, last+1)
136+ _, err = c.db.Apply(ctx, &b)
137+ if errors.Is(err, kv.ErrConflict) {
138+ continue
139+ }
140+ if err != nil {
141+ return 0, err
142+ }
143+ return last + 1, nil
144+ }
145+ return 0, domain.ErrConflict
146+}
147+
148+// ReleaseUsage reads the repository's quota gate.
149+func (c *Releases) ReleaseUsage(ctx context.Context, repo domain.RepoName) (int64, uint64, error) {
150+ item, ok := c.db.Get(ctx, relUseKey(repo))
151+ if !ok {
152+ return 0, 0, nil
153+ }
154+ var pb storagepb.ReleaseUsage
155+ if err := proto.Unmarshal(item.Value, &pb); err != nil {
156+ return 0, 0, err
157+ }
158+ return pb.GetUsed(), item.Version, nil
159+}
160+
161+// GetRelease reads a release in its repository.
162+func (c *Releases) GetRelease(ctx context.Context, repo domain.RepoName, id int64) (domain.Release, error) {
163+ item, ok := c.db.Get(ctx, relKey(repo, id))
164+ if !ok {
165+ return domain.Release{}, domain.ErrNotFound
166+ }
167+ return decodeRelease(item)
168+}
169+
170+// ReleaseRepo resolves the global id index.
171+func (c *Releases) ReleaseRepo(ctx context.Context, id int64) (domain.RepoName, error) {
172+ item, ok := c.db.Get(ctx, relIDKey(id))
173+ if !ok {
174+ return domain.RepoName{}, domain.ErrNotFound
175+ }
176+ return domain.ParseRepoName(string(item.Value))
177+}
178+
179+// GetReleaseByTag resolves a repository tag.
180+func (c *Releases) GetReleaseByTag(ctx context.Context, repo domain.RepoName, tag string) (domain.Release, error) {
181+ snap := c.db.Snapshot(ctx)
182+ idx, ok := snap.Get(ctx, relTagKey(repo, tag))
183+ if !ok {
184+ return domain.Release{}, domain.ErrNotFound
185+ }
186+ id, err := strconv.ParseInt(string(idx.Value), 10, 64)
187+ if err != nil {
188+ return domain.Release{}, err
189+ }
190+ item, ok := snap.Get(ctx, relKey(repo, id))
191+ if !ok {
192+ return domain.Release{}, domain.ErrNotFound
193+ }
194+ return decodeRelease(item)
195+}
196+
197+// ListReleases returns all releases by descending id.
198+func (c *Releases) ListReleases(ctx context.Context, repo domain.RepoName) ([]domain.Release, error) {
199+ var out []domain.Release
200+ for _, item := range c.db.Snapshot(ctx).Prefix(ctx, []byte("rel/"+repo.String()+"/")) {
201+ r, err := decodeRelease(item)
202+ if err != nil {
203+ return nil, err
204+ }
205+ out = append(out, r)
206+ }
207+ slices.Reverse(out)
208+ return out, nil
209+}
210+
211+// GetAsset reads asset metadata in every state.
212+func (c *Releases) GetAsset(ctx context.Context, id int64) (domain.ReleaseAsset, error) {
213+ item, ok := c.db.Get(ctx, relAssetKey(id))
214+ if !ok {
215+ return domain.ReleaseAsset{}, domain.ErrNotFound
216+ }
217+ return decodeReleaseAsset(item)
218+}
219+
220+// GetAssetByName resolves the unique name index.
221+func (c *Releases) GetAssetByName(ctx context.Context, repo domain.RepoName, releaseID int64, name string) (domain.ReleaseAsset, error) {
222+ snap := c.db.Snapshot(ctx)
223+ idx, ok := snap.Get(ctx, relNameKey(domain.ReleaseAsset{Repo: repo, ReleaseID: releaseID, Name: name}))
224+ if !ok {
225+ return domain.ReleaseAsset{}, domain.ErrNotFound
226+ }
227+ id, err := strconv.ParseInt(string(idx.Value), 10, 64)
228+ if err != nil {
229+ return domain.ReleaseAsset{}, err
230+ }
231+ item, ok := snap.Get(ctx, relAssetKey(id))
232+ if !ok {
233+ return domain.ReleaseAsset{}, domain.ErrNotFound
234+ }
235+ return decodeReleaseAsset(item)
236+}
237+
238+// ListAssets returns every state in name order.
239+func (c *Releases) ListAssets(ctx context.Context, repo domain.RepoName, releaseID int64) ([]domain.ReleaseAsset, error) {
240+ snap := c.db.Snapshot(ctx)
241+ var out []domain.ReleaseAsset
242+ for _, idx := range snap.Prefix(ctx, []byte(relNamePrefix(repo, releaseID))) {
243+ id, err := strconv.ParseInt(string(idx.Value), 10, 64)
244+ if err != nil {
245+ return nil, err
246+ }
247+ item, ok := snap.Get(ctx, relAssetKey(id))
248+ if !ok {
249+ return nil, domain.ErrNotFound
250+ }
251+ a, err := decodeReleaseAsset(item)
252+ if err != nil {
253+ return nil, err
254+ }
255+ out = append(out, a)
256+ }
257+ return out, nil
258+}
259+
260+// AllAssets returns every asset for garbage collection.
261+func (c *Releases) AllAssets(ctx context.Context) ([]domain.ReleaseAsset, error) {
262+ var out []domain.ReleaseAsset
263+ for _, item := range c.db.Snapshot(ctx).Prefix(ctx, []byte("relasset/")) {
264+ a, err := decodeReleaseAsset(item)
265+ if err != nil {
266+ return nil, err
267+ }
268+ out = append(out, a)
269+ }
270+ return out, nil
271+}
272+
273+// ApplyReleases atomically changes metadata and its quota gate.
274+//
275+//hexcheck:ignore suspicious-business-logic-in-adapter enforces storage version preconditions and unique secondary indexes, not release policy
276+func (c *Releases) ApplyReleases(ctx context.Context, w ports.ReleaseWrite) error {
277+ log := zerowrap.FromCtx(ctx)
278+ var b kv.Batch
279+ if err := guardRepoKey(ctx, c.db, &b, w.Repo); err != nil {
280+ return err
281+ }
282+ if w.UsageVersion == 0 {
283+ b.RequireAbsent(relUseKey(w.Repo))
284+ } else {
285+ b.RequireVersion(relUseKey(w.Repo), w.UsageVersion)
286+ }
287+ b.Put(relUseKey(w.Repo), mustMarshal(&storagepb.ReleaseUsage{Used: w.Used}))
288+ snap := c.db.Snapshot(ctx)
289+ for _, r := range w.DeleteReleases {
290+ if r.Repo != w.Repo || r.Version == 0 {
291+ return domain.ErrInvalid
292+ }
293+ b.RequireVersion(relKey(r.Repo, r.ID), r.Version)
294+ b.Delete(relKey(r.Repo, r.ID))
295+ b.Delete(relTagKey(r.Repo, r.Tag))
296+ b.Delete(relIDKey(r.ID))
297+ }
298+ for _, r := range w.DeleteAssets {
299+ if r.Repo != w.Repo || r.Version == 0 {
300+ return domain.ErrInvalid
301+ }
302+ b.RequireVersion(relAssetKey(r.ID), r.Version)
303+ b.Delete(relAssetKey(r.ID))
304+ b.Delete(relNameKey(r))
305+ }
306+ seenReleases := map[string]int64{}
307+ for _, r := range w.PutReleases {
308+ if r.Repo != w.Repo || r.ID < 1 {
309+ return domain.ErrInvalid
310+ }
311+ idxKey := relTagKey(r.Repo, r.Tag)
312+ if id, ok := seenReleases[string(idxKey)]; ok && id != r.ID {
313+ return domain.ErrAlreadyExists
314+ }
315+ seenReleases[string(idxKey)] = r.ID
316+ if item, ok := snap.Get(ctx, idxKey); ok && string(item.Value) != fmt.Sprintf(idFormat, r.ID) {
317+ removed := false
318+ for _, d := range w.DeleteReleases {
319+ if string(relTagKey(d.Repo, d.Tag)) == string(idxKey) {
320+ removed = true
321+ }
322+ }
323+ if !removed {
324+ return domain.ErrAlreadyExists
325+ }
326+ }
327+ if old, ok := snap.Get(ctx, relKey(r.Repo, r.ID)); ok {
328+ previous, err := decodeRelease(old)
329+ if err != nil {
330+ return err
331+ }
332+ oldKey := relTagKey(previous.Repo, previous.Tag)
333+ if string(oldKey) != string(idxKey) {
334+ b.Delete(oldKey)
335+ }
336+ }
337+ stage(&b, relKey(r.Repo, r.ID), encodeRelease(r), r.Version)
338+ b.Put(idxKey, []byte(fmt.Sprintf(idFormat, r.ID)))
339+ if r.Version == 0 {
340+ b.RequireAbsent(relIDKey(r.ID))
341+ }
342+ b.Put(relIDKey(r.ID), []byte(r.Repo.String()))
343+ }
344+ seenAssets := map[string]int64{}
345+ for _, r := range w.PutAssets {
346+ if r.Repo != w.Repo || r.ID < 1 {
347+ return domain.ErrInvalid
348+ }
349+ idxKey := relNameKey(r)
350+ if id, ok := seenAssets[string(idxKey)]; ok && id != r.ID {
351+ return domain.ErrAlreadyExists
352+ }
353+ seenAssets[string(idxKey)] = r.ID
354+ if item, ok := snap.Get(ctx, idxKey); ok && string(item.Value) != fmt.Sprintf(idFormat, r.ID) {
355+ removed := false
356+ for _, d := range w.DeleteAssets {
357+ if string(relNameKey(d)) == string(idxKey) {
358+ removed = true
359+ }
360+ }
361+ if !removed {
362+ return domain.ErrAlreadyExists
363+ }
364+ }
365+ if old, ok := snap.Get(ctx, relAssetKey(r.ID)); ok {
366+ previous, err := decodeReleaseAsset(old)
367+ if err != nil {
368+ return err
369+ }
370+ oldKey := relNameKey(previous)
371+ if string(oldKey) != string(idxKey) {
372+ b.Delete(oldKey)
373+ }
374+ }
375+ stage(&b, relAssetKey(r.ID), encodeReleaseAsset(r), r.Version)
376+ b.Put(idxKey, []byte(fmt.Sprintf(idFormat, r.ID)))
377+ }
378+ _, err := c.db.Apply(ctx, &b)
379+ if errors.Is(err, kv.ErrConflict) {
380+ return domain.ErrConflict
381+ }
382+ if err != nil {
383+ return fmt.Errorf("write releases: %w", err)
384+ }
385+ log.Debug().Str("repo", w.Repo.String()).Msg("release metadata updated")
386+ return nil
387+}
388+
389+// releaseKeys collects all repository-owned release keys, including global indexes.
390+func releaseKeys(ctx context.Context, snap *kv.Snapshot, repo domain.RepoName) [][]byte {
391+ keys := [][]byte{relUseKey(repo)}
392+ for _, prefix := range []string{"rel/", "reltag/", "relassets/"} {
393+ for key, item := range snap.Prefix(ctx, []byte(prefix+repo.String()+"/")) {
394+ keys = append(keys, slices.Clone(key))
395+ if prefix == "rel/" {
396+ if r, err := decodeRelease(item); err == nil {
397+ keys = append(keys, relIDKey(r.ID))
398+ }
399+ }
400+ }
401+ }
402+ for key, item := range snap.Prefix(ctx, []byte("relasset/")) {
403+ if a, err := decodeReleaseAsset(item); err == nil && a.Repo == repo {
404+ keys = append(keys, slices.Clone(key))
405+ }
406+ }
407+ return keys
408+}
@@ -0,0 +1,408 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+package kvstore
3+
4+import (
5+ kv "bnema.dev/ermite-kv"
6+ storagepb "bnema.dev/ermite/gen/ermite/storage"
7+ "bnema.dev/ermite/internal/domain"
8+ "bnema.dev/ermite/internal/ports"
9+ "context"
10+ "errors"
11+ "fmt"
12+ "github.com/bnema/zerowrap"
13+ "google.golang.org/protobuf/proto"
14+ "slices"
15+ "strconv"
16+)
17+
18+// Releases implements ports.ReleaseStore over the shared metadata database.
19+type Releases struct{ db *kv.DB }
20+
21+var _ ports.ReleaseStore = (*Releases)(nil)
22+
23+// Releases returns the release metadata store.
24+func (s *Store) Releases() *Releases { return &Releases{db: s.db} }
25+func relKey(repo domain.RepoName, id int64) []byte {
26+ return []byte("rel/" + repo.String() + "/" + fmt.Sprintf(idFormat, id))
27+}
28+func relIDKey(id int64) []byte { return []byte("relid/" + fmt.Sprintf(idFormat, id)) }
29+func relTagKey(repo domain.RepoName, tag string) []byte {
30+ return []byte("reltag/" + repo.String() + "/" + tag)
31+}
32+func relAssetKey(id int64) []byte { return []byte("relasset/" + fmt.Sprintf(idFormat, id)) }
33+func relNamePrefix(repo domain.RepoName, id int64) string {
34+ return "relassets/" + repo.String() + "/" + fmt.Sprintf(idFormat, id) + "/"
35+}
36+func relNameKey(a domain.ReleaseAsset) []byte {
37+ return []byte(relNamePrefix(a.Repo, a.ReleaseID) + a.Name)
38+}
39+func relUseKey(repo domain.RepoName) []byte { return []byte("reluse/" + repo.String()) }
40+func encodeRelease(r domain.Release) []byte {
41+ return mustMarshal(&storagepb.Release{
42+ Id: r.ID,
43+ Repo: r.Repo.String(),
44+ Tag: r.Tag,
45+ Title: r.Title,
46+ Notes: r.Notes,
47+ Draft: r.Draft,
48+ Prerelease: r.Prerelease,
49+ MakeLatest: r.MakeLatest,
50+ TargetCommitish: r.TargetCommitish,
51+ Author: r.Author,
52+ CreatedAt: encodeTime(r.CreatedAt),
53+ PublishedAt: encodeTime(r.PublishedAt),
54+ UpdatedAt: encodeTime(r.UpdatedAt),
55+ })
56+}
57+func decodeRelease(item kv.Item) (domain.Release, error) {
58+ var pb storagepb.Release
59+ if err := proto.Unmarshal(item.Value, &pb); err != nil {
60+ return domain.Release{}, err
61+ }
62+ repo, err := domain.ParseRepoName(pb.GetRepo())
63+ if err != nil {
64+ return domain.Release{}, err
65+ }
66+ return domain.Release{Version: item.Version,
67+ ID: pb.GetId(),
68+ Repo: repo,
69+ Tag: pb.GetTag(),
70+ Title: pb.GetTitle(),
71+ Notes: pb.GetNotes(),
72+ Draft: pb.GetDraft(),
73+ Prerelease: pb.GetPrerelease(),
74+ MakeLatest: pb.GetMakeLatest(),
75+ TargetCommitish: pb.GetTargetCommitish(),
76+ Author: pb.GetAuthor(),
77+ CreatedAt: decodeTime(pb.GetCreatedAt()),
78+ PublishedAt: decodeTime(pb.GetPublishedAt()),
79+ UpdatedAt: decodeTime(pb.GetUpdatedAt()),
80+ }, nil
81+}
82+func encodeReleaseAsset(r domain.ReleaseAsset) []byte {
83+ return mustMarshal(&storagepb.ReleaseAsset{
84+ Id: r.ID,
85+ ReleaseId: r.ReleaseID,
86+ Repo: r.Repo.String(),
87+ Name: r.Name,
88+ Label: r.Label,
89+ ContentType: r.ContentType,
90+ Size: r.Size,
91+ Digest: r.Digest,
92+ State: string(r.State),
93+ Uploader: r.Uploader,
94+ Charged: r.Charged,
95+ MultipartId: r.MultipartID,
96+ CreatedAt: encodeTime(r.CreatedAt),
97+ UpdatedAt: encodeTime(r.UpdatedAt),
98+ })
99+}
100+func decodeReleaseAsset(item kv.Item) (domain.ReleaseAsset, error) {
101+ var pb storagepb.ReleaseAsset
102+ if err := proto.Unmarshal(item.Value, &pb); err != nil {
103+ return domain.ReleaseAsset{}, err
104+ }
105+ repo, err := domain.ParseRepoName(pb.GetRepo())
106+ if err != nil {
107+ return domain.ReleaseAsset{}, err
108+ }
109+ return domain.ReleaseAsset{Version: item.Version,
110+ ID: pb.GetId(),
111+ ReleaseID: pb.GetReleaseId(),
112+ Repo: repo,
113+ Name: pb.GetName(),
114+ Label: pb.GetLabel(),
115+ ContentType: pb.GetContentType(),
116+ Size: pb.GetSize(),
117+ Digest: pb.GetDigest(),
118+ State: domain.ReleaseAssetState(pb.GetState()),
119+ Uploader: pb.GetUploader(),
120+ Charged: pb.GetCharged(),
121+ MultipartID: pb.GetMultipartId(),
122+ CreatedAt: decodeTime(pb.GetCreatedAt()),
123+ UpdatedAt: decodeTime(pb.GetUpdatedAt()),
124+ }, nil
125+}
126+
127+// NextReleaseID allocates a global monotonically increasing id.
128+func (c *Releases) NextReleaseID(ctx context.Context) (int64, error) {
129+ for range artifactIDTries {
130+ last, item, ok, err := (&CI{db: c.db}).readSeq(ctx, []byte("relseq"))
131+ if err != nil {
132+ return 0, err
133+ }
134+ var b kv.Batch
135+ stageSeq(&b, []byte("relseq"), item, ok, last+1)
136+ _, err = c.db.Apply(ctx, &b)
137+ if errors.Is(err, kv.ErrConflict) {
138+ continue
139+ }
140+ if err != nil {
141+ return 0, err
142+ }
143+ return last + 1, nil
144+ }
145+ return 0, domain.ErrConflict
146+}
147+
148+// ReleaseUsage reads the repository's quota gate.
149+func (c *Releases) ReleaseUsage(ctx context.Context, repo domain.RepoName) (int64, uint64, error) {
150+ item, ok := c.db.Get(ctx, relUseKey(repo))
151+ if !ok {
152+ return 0, 0, nil
153+ }
154+ var pb storagepb.ReleaseUsage
155+ if err := proto.Unmarshal(item.Value, &pb); err != nil {
156+ return 0, 0, err
157+ }
158+ return pb.GetUsed(), item.Version, nil
159+}
160+
161+// GetRelease reads a release in its repository.
162+func (c *Releases) GetRelease(ctx context.Context, repo domain.RepoName, id int64) (domain.Release, error) {
163+ item, ok := c.db.Get(ctx, relKey(repo, id))
164+ if !ok {
165+ return domain.Release{}, domain.ErrNotFound
166+ }
167+ return decodeRelease(item)
168+}
169+
170+// ReleaseRepo resolves the global id index.
171+func (c *Releases) ReleaseRepo(ctx context.Context, id int64) (domain.RepoName, error) {
172+ item, ok := c.db.Get(ctx, relIDKey(id))
173+ if !ok {
174+ return domain.RepoName{}, domain.ErrNotFound
175+ }
176+ return domain.ParseRepoName(string(item.Value))
177+}
178+
179+// GetReleaseByTag resolves a repository tag.
180+func (c *Releases) GetReleaseByTag(ctx context.Context, repo domain.RepoName, tag string) (domain.Release, error) {
181+ snap := c.db.Snapshot(ctx)
182+ idx, ok := snap.Get(ctx, relTagKey(repo, tag))
183+ if !ok {
184+ return domain.Release{}, domain.ErrNotFound
185+ }
186+ id, err := strconv.ParseInt(string(idx.Value), 10, 64)
187+ if err != nil {
188+ return domain.Release{}, err
189+ }
190+ item, ok := snap.Get(ctx, relKey(repo, id))
191+ if !ok {
192+ return domain.Release{}, domain.ErrNotFound
193+ }
194+ return decodeRelease(item)
195+}
196+
197+// ListReleases returns all releases by descending id.
198+func (c *Releases) ListReleases(ctx context.Context, repo domain.RepoName) ([]domain.Release, error) {
199+ var out []domain.Release
200+ for _, item := range c.db.Snapshot(ctx).Prefix(ctx, []byte("rel/"+repo.String()+"/")) {
201+ r, err := decodeRelease(item)
202+ if err != nil {
203+ return nil, err
204+ }
205+ out = append(out, r)
206+ }
207+ slices.Reverse(out)
208+ return out, nil
209+}
210+
211+// GetAsset reads asset metadata in every state.
212+func (c *Releases) GetAsset(ctx context.Context, id int64) (domain.ReleaseAsset, error) {
213+ item, ok := c.db.Get(ctx, relAssetKey(id))
214+ if !ok {
215+ return domain.ReleaseAsset{}, domain.ErrNotFound
216+ }
217+ return decodeReleaseAsset(item)
218+}
219+
220+// GetAssetByName resolves the unique name index.
221+func (c *Releases) GetAssetByName(ctx context.Context, repo domain.RepoName, releaseID int64, name string) (domain.ReleaseAsset, error) {
222+ snap := c.db.Snapshot(ctx)
223+ idx, ok := snap.Get(ctx, relNameKey(domain.ReleaseAsset{Repo: repo, ReleaseID: releaseID, Name: name}))
224+ if !ok {
225+ return domain.ReleaseAsset{}, domain.ErrNotFound
226+ }
227+ id, err := strconv.ParseInt(string(idx.Value), 10, 64)
228+ if err != nil {
229+ return domain.ReleaseAsset{}, err
230+ }
231+ item, ok := snap.Get(ctx, relAssetKey(id))
232+ if !ok {
233+ return domain.ReleaseAsset{}, domain.ErrNotFound
234+ }
235+ return decodeReleaseAsset(item)
236+}
237+
238+// ListAssets returns every state in name order.
239+func (c *Releases) ListAssets(ctx context.Context, repo domain.RepoName, releaseID int64) ([]domain.ReleaseAsset, error) {
240+ snap := c.db.Snapshot(ctx)
241+ var out []domain.ReleaseAsset
242+ for _, idx := range snap.Prefix(ctx, []byte(relNamePrefix(repo, releaseID))) {
243+ id, err := strconv.ParseInt(string(idx.Value), 10, 64)
244+ if err != nil {
245+ return nil, err
246+ }
247+ item, ok := snap.Get(ctx, relAssetKey(id))
248+ if !ok {
249+ return nil, domain.ErrNotFound
250+ }
251+ a, err := decodeReleaseAsset(item)
252+ if err != nil {
253+ return nil, err
254+ }
255+ out = append(out, a)
256+ }
257+ return out, nil
258+}
259+
260+// AllAssets returns every asset for garbage collection.
261+func (c *Releases) AllAssets(ctx context.Context) ([]domain.ReleaseAsset, error) {
262+ var out []domain.ReleaseAsset
263+ for _, item := range c.db.Snapshot(ctx).Prefix(ctx, []byte("relasset/")) {
264+ a, err := decodeReleaseAsset(item)
265+ if err != nil {
266+ return nil, err
267+ }
268+ out = append(out, a)
269+ }
270+ return out, nil
271+}
272+
273+// ApplyReleases atomically changes metadata and its quota gate.
274+//
275+//hexcheck:ignore suspicious-business-logic-in-adapter enforces storage version preconditions and unique secondary indexes, not release policy
276+func (c *Releases) ApplyReleases(ctx context.Context, w ports.ReleaseWrite) error {
277+ log := zerowrap.FromCtx(ctx)
278+ var b kv.Batch
279+ if err := guardRepoKey(ctx, c.db, &b, w.Repo); err != nil {
280+ return err
281+ }
282+ if w.UsageVersion == 0 {
283+ b.RequireAbsent(relUseKey(w.Repo))
284+ } else {
285+ b.RequireVersion(relUseKey(w.Repo), w.UsageVersion)
286+ }
287+ b.Put(relUseKey(w.Repo), mustMarshal(&storagepb.ReleaseUsage{Used: w.Used}))
288+ snap := c.db.Snapshot(ctx)
289+ for _, r := range w.DeleteReleases {
290+ if r.Repo != w.Repo || r.Version == 0 {
291+ return domain.ErrInvalid
292+ }
293+ b.RequireVersion(relKey(r.Repo, r.ID), r.Version)
294+ b.Delete(relKey(r.Repo, r.ID))
295+ b.Delete(relTagKey(r.Repo, r.Tag))
296+ b.Delete(relIDKey(r.ID))
297+ }
298+ for _, r := range w.DeleteAssets {
299+ if r.Repo != w.Repo || r.Version == 0 {
300+ return domain.ErrInvalid
301+ }
302+ b.RequireVersion(relAssetKey(r.ID), r.Version)
303+ b.Delete(relAssetKey(r.ID))
304+ b.Delete(relNameKey(r))
305+ }
306+ seenReleases := map[string]int64{}
307+ for _, r := range w.PutReleases {
308+ if r.Repo != w.Repo || r.ID < 1 {
309+ return domain.ErrInvalid
310+ }
311+ idxKey := relTagKey(r.Repo, r.Tag)
312+ if id, ok := seenReleases[string(idxKey)]; ok && id != r.ID {
313+ return domain.ErrAlreadyExists
314+ }
315+ seenReleases[string(idxKey)] = r.ID
316+ if item, ok := snap.Get(ctx, idxKey); ok && string(item.Value) != fmt.Sprintf(idFormat, r.ID) {
317+ removed := false
318+ for _, d := range w.DeleteReleases {
319+ if string(relTagKey(d.Repo, d.Tag)) == string(idxKey) {
320+ removed = true
321+ }
322+ }
323+ if !removed {
324+ return domain.ErrAlreadyExists
325+ }
326+ }
327+ if old, ok := snap.Get(ctx, relKey(r.Repo, r.ID)); ok {
328+ previous, err := decodeRelease(old)
329+ if err != nil {
330+ return err
331+ }
332+ oldKey := relTagKey(previous.Repo, previous.Tag)
333+ if string(oldKey) != string(idxKey) {
334+ b.Delete(oldKey)
335+ }
336+ }
337+ stage(&b, relKey(r.Repo, r.ID), encodeRelease(r), r.Version)
338+ b.Put(idxKey, []byte(fmt.Sprintf(idFormat, r.ID)))
339+ if r.Version == 0 {
340+ b.RequireAbsent(relIDKey(r.ID))
341+ }
342+ b.Put(relIDKey(r.ID), []byte(r.Repo.String()))
343+ }
344+ seenAssets := map[string]int64{}
345+ for _, r := range w.PutAssets {
346+ if r.Repo != w.Repo || r.ID < 1 {
347+ return domain.ErrInvalid
348+ }
349+ idxKey := relNameKey(r)
350+ if id, ok := seenAssets[string(idxKey)]; ok && id != r.ID {
351+ return domain.ErrAlreadyExists
352+ }
353+ seenAssets[string(idxKey)] = r.ID
354+ if item, ok := snap.Get(ctx, idxKey); ok && string(item.Value) != fmt.Sprintf(idFormat, r.ID) {
355+ removed := false
356+ for _, d := range w.DeleteAssets {
357+ if string(relNameKey(d)) == string(idxKey) {
358+ removed = true
359+ }
360+ }
361+ if !removed {
362+ return domain.ErrAlreadyExists
363+ }
364+ }
365+ if old, ok := snap.Get(ctx, relAssetKey(r.ID)); ok {
366+ previous, err := decodeReleaseAsset(old)
367+ if err != nil {
368+ return err
369+ }
370+ oldKey := relNameKey(previous)
371+ if string(oldKey) != string(idxKey) {
372+ b.Delete(oldKey)
373+ }
374+ }
375+ stage(&b, relAssetKey(r.ID), encodeReleaseAsset(r), r.Version)
376+ b.Put(idxKey, []byte(fmt.Sprintf(idFormat, r.ID)))
377+ }
378+ _, err := c.db.Apply(ctx, &b)
379+ if errors.Is(err, kv.ErrConflict) {
380+ return domain.ErrConflict
381+ }
382+ if err != nil {
383+ return fmt.Errorf("write releases: %w", err)
384+ }
385+ log.Debug().Str("repo", w.Repo.String()).Msg("release metadata updated")
386+ return nil
387+}
388+
389+// releaseKeys collects all repository-owned release keys, including global indexes.
390+func releaseKeys(ctx context.Context, snap *kv.Snapshot, repo domain.RepoName) [][]byte {
391+ keys := [][]byte{relUseKey(repo)}
392+ for _, prefix := range []string{"rel/", "reltag/", "relassets/"} {
393+ for key, item := range snap.Prefix(ctx, []byte(prefix+repo.String()+"/")) {
394+ keys = append(keys, slices.Clone(key))
395+ if prefix == "rel/" {
396+ if r, err := decodeRelease(item); err == nil {
397+ keys = append(keys, relIDKey(r.ID))
398+ }
399+ }
400+ }
401+ }
402+ for key, item := range snap.Prefix(ctx, []byte("relasset/")) {
403+ if a, err := decodeReleaseAsset(item); err == nil && a.Repo == repo {
404+ keys = append(keys, slices.Clone(key))
405+ }
406+ }
407+ return keys
408+}
A internal/adapters/kvstore/releases_test.go +131 -0
@@ -0,0 +1,131 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+package kvstore_test
3+
4+import (
5+ "context"
6+ "sync"
7+ "testing"
8+
9+ kv "bnema.dev/ermite-kv"
10+ "github.com/stretchr/testify/require"
11+
12+ "bnema.dev/ermite/internal/domain"
13+ "bnema.dev/ermite/internal/ports"
14+)
15+
16+func TestReleaseStorage(t *testing.T) {
17+ ctx := context.Background()
18+ dir := t.TempDir()
19+ s := openStore(t, dir)
20+ repo := name(t, "release-test")
21+ require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPublic, CreatedAt: created, Settings: domain.DefaultRepoSettings()}))
22+ st := s.Releases()
23+ id, err := st.NextReleaseID(ctx)
24+ require.NoError(t, err)
25+ r := domain.Release{ID: id, Repo: repo, Tag: "v1", Title: "title", Notes: "notes", MakeLatest: true, TargetCommitish: "abc", Author: "owner", CreatedAt: created, PublishedAt: created, UpdatedAt: created}
26+ assetID, err := st.NextReleaseID(ctx)
27+ require.NoError(t, err)
28+ a := domain.ReleaseAsset{ID: assetID, ReleaseID: id, Repo: repo, Name: "binary", Label: "label", ContentType: "application/octet-stream", Size: 3, Digest: "sha256:abc", State: domain.ReleaseAssetUploading, Uploader: "owner", Charged: 3, MultipartID: "multipart", CreatedAt: created, UpdatedAt: created}
29+ w := ports.ReleaseWrite{Repo: repo, Used: 3, PutReleases: []domain.Release{r}, PutAssets: []domain.ReleaseAsset{a}}
30+ require.NoError(t, st.ApplyReleases(ctx, w))
31+ got, err := st.GetReleaseByTag(ctx, repo, "v1")
32+ require.NoError(t, err)
33+ r.Version = got.Version
34+ require.Equal(t, r, got)
35+ ar, err := st.GetAssetByName(ctx, repo, id, "binary")
36+ require.NoError(t, err)
37+ a.Version = ar.Version
38+ require.Equal(t, a, ar)
39+ rr, err := st.ReleaseRepo(ctx, id)
40+ require.NoError(t, err)
41+ require.Equal(t, repo, rr)
42+ rs, err := st.ListReleases(ctx, repo)
43+ require.NoError(t, err)
44+ require.Equal(t, []domain.Release{r}, rs)
45+ as, err := st.ListAssets(ctx, repo, id)
46+ require.NoError(t, err)
47+ require.Equal(t, []domain.ReleaseAsset{a}, as)
48+ all, err := st.AllAssets(ctx)
49+ require.NoError(t, err)
50+ require.Equal(t, as, all)
51+ require.ErrorIs(t, st.ApplyReleases(ctx, w), domain.ErrConflict)
52+ used, version, err := st.ReleaseUsage(ctx, repo)
53+ require.NoError(t, err)
54+ require.EqualValues(t, 3, used)
55+ duplicate := r
56+ duplicate.ID += 10
57+ duplicate.Version = 0
58+ require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, PutReleases: []domain.Release{duplicate}}), domain.ErrAlreadyExists)
59+ da := a
60+ da.ID += 10
61+ da.Version = 0
62+ require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, PutAssets: []domain.ReleaseAsset{da}}), domain.ErrAlreadyExists)
63+ stale := r
64+ stale.Version++
65+ require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, PutReleases: []domain.Release{stale}}), domain.ErrConflict)
66+ staleAsset := a
67+ staleAsset.Version++
68+ require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, DeleteAssets: []domain.ReleaseAsset{staleAsset}}), domain.ErrConflict)
69+ r.Tag = "v2"
70+ a.Name = "renamed"
71+ require.NoError(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, Used: 3, PutReleases: []domain.Release{r}, PutAssets: []domain.ReleaseAsset{a}}))
72+ _, err = st.GetReleaseByTag(ctx, repo, "v1")
73+ require.ErrorIs(t, err, domain.ErrNotFound)
74+ _, err = st.GetAssetByName(ctx, repo, id, "binary")
75+ require.ErrorIs(t, err, domain.ErrNotFound)
76+ renamedRelease, err := st.GetReleaseByTag(ctx, repo, "v2")
77+ require.NoError(t, err)
78+ renamedAsset, err := st.GetAssetByName(ctx, repo, id, "renamed")
79+ require.NoError(t, err)
80+ _, version, err = st.ReleaseUsage(ctx, repo)
81+ require.NoError(t, err)
82+ require.NoError(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, DeleteReleases: []domain.Release{renamedRelease}, DeleteAssets: []domain.ReleaseAsset{renamedAsset}}))
83+ _, err = st.GetRelease(ctx, repo, id)
84+ require.ErrorIs(t, err, domain.ErrNotFound)
85+ _, err = st.GetAsset(ctx, a.ID)
86+ require.ErrorIs(t, err, domain.ErrNotFound)
87+ _, version, err = st.ReleaseUsage(ctx, repo)
88+ require.NoError(t, err)
89+ r.Version, a.Version = 0, 0
90+ require.NoError(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, Used: 3, PutReleases: []domain.Release{r}, PutAssets: []domain.ReleaseAsset{a}}))
91+ require.NoError(t, s.Delete(ctx, repo))
92+ _, err = st.GetRelease(ctx, repo, id)
93+ require.ErrorIs(t, err, domain.ErrNotFound)
94+ _, err = st.ReleaseRepo(ctx, id)
95+ require.ErrorIs(t, err, domain.ErrNotFound)
96+ _, err = st.GetAsset(ctx, a.ID)
97+ require.ErrorIs(t, err, domain.ErrNotFound)
98+ all, err = st.AllAssets(ctx)
99+ require.NoError(t, err)
100+ require.Empty(t, all)
101+ used, version, err = st.ReleaseUsage(ctx, repo)
102+ require.NoError(t, err)
103+ require.Zero(t, used)
104+ require.Zero(t, version)
105+ require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo}), domain.ErrNotFound)
106+ require.NoError(t, s.Close(ctx))
107+ db, err := kv.Open(ctx, dir, kv.Options{})
108+ require.NoError(t, err)
109+ defer db.Close(ctx)
110+ for _, prefix := range []string{"rel/", "reltag/", "relid/", "relasset/", "relassets/", "reluse/"} {
111+ for key := range db.Snapshot(ctx).Prefix(ctx, []byte(prefix)) {
112+ t.Errorf("repository deletion left release key %q", key)
113+ }
114+ }
115+}
116+func TestConcurrentReleaseIDs(t *testing.T) {
117+ st := openStore(t, t.TempDir()).Releases()
118+ var wg sync.WaitGroup
119+ ids := make(chan int64, 32)
120+ for range 32 {
121+ wg.Go(func() { id, err := st.NextReleaseID(context.Background()); require.NoError(t, err); ids <- id })
122+ }
123+ wg.Wait()
124+ close(ids)
125+ seen := map[int64]bool{}
126+ for id := range ids {
127+ require.False(t, seen[id])
128+ seen[id] = true
129+ }
130+ require.Len(t, seen, 32)
131+}
@@ -0,0 +1,131 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+package kvstore_test
3+
4+import (
5+ "context"
6+ "sync"
7+ "testing"
8+
9+ kv "bnema.dev/ermite-kv"
10+ "github.com/stretchr/testify/require"
11+
12+ "bnema.dev/ermite/internal/domain"
13+ "bnema.dev/ermite/internal/ports"
14+)
15+
16+func TestReleaseStorage(t *testing.T) {
17+ ctx := context.Background()
18+ dir := t.TempDir()
19+ s := openStore(t, dir)
20+ repo := name(t, "release-test")
21+ require.NoError(t, s.Create(ctx, domain.Repo{Name: repo, Visibility: domain.VisibilityPublic, CreatedAt: created, Settings: domain.DefaultRepoSettings()}))
22+ st := s.Releases()
23+ id, err := st.NextReleaseID(ctx)
24+ require.NoError(t, err)
25+ r := domain.Release{ID: id, Repo: repo, Tag: "v1", Title: "title", Notes: "notes", MakeLatest: true, TargetCommitish: "abc", Author: "owner", CreatedAt: created, PublishedAt: created, UpdatedAt: created}
26+ assetID, err := st.NextReleaseID(ctx)
27+ require.NoError(t, err)
28+ a := domain.ReleaseAsset{ID: assetID, ReleaseID: id, Repo: repo, Name: "binary", Label: "label", ContentType: "application/octet-stream", Size: 3, Digest: "sha256:abc", State: domain.ReleaseAssetUploading, Uploader: "owner", Charged: 3, MultipartID: "multipart", CreatedAt: created, UpdatedAt: created}
29+ w := ports.ReleaseWrite{Repo: repo, Used: 3, PutReleases: []domain.Release{r}, PutAssets: []domain.ReleaseAsset{a}}
30+ require.NoError(t, st.ApplyReleases(ctx, w))
31+ got, err := st.GetReleaseByTag(ctx, repo, "v1")
32+ require.NoError(t, err)
33+ r.Version = got.Version
34+ require.Equal(t, r, got)
35+ ar, err := st.GetAssetByName(ctx, repo, id, "binary")
36+ require.NoError(t, err)
37+ a.Version = ar.Version
38+ require.Equal(t, a, ar)
39+ rr, err := st.ReleaseRepo(ctx, id)
40+ require.NoError(t, err)
41+ require.Equal(t, repo, rr)
42+ rs, err := st.ListReleases(ctx, repo)
43+ require.NoError(t, err)
44+ require.Equal(t, []domain.Release{r}, rs)
45+ as, err := st.ListAssets(ctx, repo, id)
46+ require.NoError(t, err)
47+ require.Equal(t, []domain.ReleaseAsset{a}, as)
48+ all, err := st.AllAssets(ctx)
49+ require.NoError(t, err)
50+ require.Equal(t, as, all)
51+ require.ErrorIs(t, st.ApplyReleases(ctx, w), domain.ErrConflict)
52+ used, version, err := st.ReleaseUsage(ctx, repo)
53+ require.NoError(t, err)
54+ require.EqualValues(t, 3, used)
55+ duplicate := r
56+ duplicate.ID += 10
57+ duplicate.Version = 0
58+ require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, PutReleases: []domain.Release{duplicate}}), domain.ErrAlreadyExists)
59+ da := a
60+ da.ID += 10
61+ da.Version = 0
62+ require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, PutAssets: []domain.ReleaseAsset{da}}), domain.ErrAlreadyExists)
63+ stale := r
64+ stale.Version++
65+ require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, PutReleases: []domain.Release{stale}}), domain.ErrConflict)
66+ staleAsset := a
67+ staleAsset.Version++
68+ require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, DeleteAssets: []domain.ReleaseAsset{staleAsset}}), domain.ErrConflict)
69+ r.Tag = "v2"
70+ a.Name = "renamed"
71+ require.NoError(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, Used: 3, PutReleases: []domain.Release{r}, PutAssets: []domain.ReleaseAsset{a}}))
72+ _, err = st.GetReleaseByTag(ctx, repo, "v1")
73+ require.ErrorIs(t, err, domain.ErrNotFound)
74+ _, err = st.GetAssetByName(ctx, repo, id, "binary")
75+ require.ErrorIs(t, err, domain.ErrNotFound)
76+ renamedRelease, err := st.GetReleaseByTag(ctx, repo, "v2")
77+ require.NoError(t, err)
78+ renamedAsset, err := st.GetAssetByName(ctx, repo, id, "renamed")
79+ require.NoError(t, err)
80+ _, version, err = st.ReleaseUsage(ctx, repo)
81+ require.NoError(t, err)
82+ require.NoError(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, DeleteReleases: []domain.Release{renamedRelease}, DeleteAssets: []domain.ReleaseAsset{renamedAsset}}))
83+ _, err = st.GetRelease(ctx, repo, id)
84+ require.ErrorIs(t, err, domain.ErrNotFound)
85+ _, err = st.GetAsset(ctx, a.ID)
86+ require.ErrorIs(t, err, domain.ErrNotFound)
87+ _, version, err = st.ReleaseUsage(ctx, repo)
88+ require.NoError(t, err)
89+ r.Version, a.Version = 0, 0
90+ require.NoError(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo, UsageVersion: version, Used: 3, PutReleases: []domain.Release{r}, PutAssets: []domain.ReleaseAsset{a}}))
91+ require.NoError(t, s.Delete(ctx, repo))
92+ _, err = st.GetRelease(ctx, repo, id)
93+ require.ErrorIs(t, err, domain.ErrNotFound)
94+ _, err = st.ReleaseRepo(ctx, id)
95+ require.ErrorIs(t, err, domain.ErrNotFound)
96+ _, err = st.GetAsset(ctx, a.ID)
97+ require.ErrorIs(t, err, domain.ErrNotFound)
98+ all, err = st.AllAssets(ctx)
99+ require.NoError(t, err)
100+ require.Empty(t, all)
101+ used, version, err = st.ReleaseUsage(ctx, repo)
102+ require.NoError(t, err)
103+ require.Zero(t, used)
104+ require.Zero(t, version)
105+ require.ErrorIs(t, st.ApplyReleases(ctx, ports.ReleaseWrite{Repo: repo}), domain.ErrNotFound)
106+ require.NoError(t, s.Close(ctx))
107+ db, err := kv.Open(ctx, dir, kv.Options{})
108+ require.NoError(t, err)
109+ defer db.Close(ctx)
110+ for _, prefix := range []string{"rel/", "reltag/", "relid/", "relasset/", "relassets/", "reluse/"} {
111+ for key := range db.Snapshot(ctx).Prefix(ctx, []byte(prefix)) {
112+ t.Errorf("repository deletion left release key %q", key)
113+ }
114+ }
115+}
116+func TestConcurrentReleaseIDs(t *testing.T) {
117+ st := openStore(t, t.TempDir()).Releases()
118+ var wg sync.WaitGroup
119+ ids := make(chan int64, 32)
120+ for range 32 {
121+ wg.Go(func() { id, err := st.NextReleaseID(context.Background()); require.NoError(t, err); ids <- id })
122+ }
123+ wg.Wait()
124+ close(ids)
125+ seen := map[int64]bool{}
126+ for id := range ids {
127+ require.False(t, seen[id])
128+ seen[id] = true
129+ }
130+ require.Len(t, seen, 32)
131+}
M internal/adapters/kvstore/repos.go +1 -0
@@ -163,6 +163,7 @@ func (s *Store) Delete(ctx context.Context, name domain.RepoName) error {
163163 keys := append(mergeRequestKeys(ctx, snap, name), issueKeys(ctx, snap, name)...)
164164 keys = append(keys, pendingKeys(ctx, snap, name)...)
165165 keys = append(keys, ciKeys(ctx, snap, name)...)
166+ keys = append(keys, releaseKeys(ctx, snap, name)...)
166167 keys = append(keys, settingsKeys(ctx, snap, name)...)
167168 keys = append(keys, mirrorKeys(ctx, snap, name)...)
168169 for len(keys) > deleteChunk {
@@ -163,6 +163,7 @@ func (s *Store) Delete(ctx context.Context, name domain.RepoName) error {
163 keys := append(mergeRequestKeys(ctx, snap, name), issueKeys(ctx, snap, name)...)163 keys := append(mergeRequestKeys(ctx, snap, name), issueKeys(ctx, snap, name)...)
164 keys = append(keys, pendingKeys(ctx, snap, name)...)164 keys = append(keys, pendingKeys(ctx, snap, name)...)
165 keys = append(keys, ciKeys(ctx, snap, name)...)165 keys = append(keys, ciKeys(ctx, snap, name)...)
166+ keys = append(keys, releaseKeys(ctx, snap, name)...)
166 keys = append(keys, settingsKeys(ctx, snap, name)...)167 keys = append(keys, settingsKeys(ctx, snap, name)...)
167 keys = append(keys, mirrorKeys(ctx, snap, name)...)168 keys = append(keys, mirrorKeys(ctx, snap, name)...)
168 for len(keys) > deleteChunk {169 for len(keys) > deleteChunk {
M internal/adapters/kvstore/store.go +25 -4
@@ -40,8 +40,10 @@ import (
4040 // if missing) to the stored tokens that held all nine original scopes with no
4141 // repository list and no branch prefix, so that they keep the owner rights (see
4242 // upgradeTokens). Version 5 grants mr:force-merge to unrestricted tokens
43-// holding all eleven previous scopes, preserving their owner rights.
44-const SchemaVersion = 5
43+// holding all eleven previous scopes, preserving their owner rights. Version 6
44+// adds releases and release:write; older builds cannot clean up their metadata
45+// or preserve the owner rights of upgraded tokens (see upgradeReleaseTokens).
46+const SchemaVersion = 6
4547
4648 const schemaKey = "meta/schema"
4749
@@ -110,7 +112,7 @@ func (s *Store) Check(ctx context.Context) error {
110112 func (s *Store) checkSchema(ctx context.Context) error {
111113 log := zerowrap.FromCtx(ctx)
112114 key := []byte(schemaKey)
113- for range 4 {
115+ for range 5 {
114116 item, ok := s.db.Get(ctx, key)
115117 if !ok {
116118 var b kv.Batch
@@ -142,8 +144,11 @@ func (s *Store) checkSchema(ctx context.Context) error {
142144 // The tokens are upgraded in the same batch, so that a crash
143145 // leaves the old version and the upgrade runs again.
144146 target := uint32(SchemaVersion)
145- if sv.GetVersion() < 4 {
147+ switch {
148+ case sv.GetVersion() < 4:
146149 target = 4
150+ case sv.GetVersion() == 4:
151+ target = 5
147152 }
148153 var b kv.Batch
149154 b.RequireVersion(key, item.Version)
@@ -157,6 +162,11 @@ func (s *Store) checkSchema(ctx context.Context) error {
157162 return err
158163 }
159164 }
165+ if sv.GetVersion() == 5 {
166+ if err := s.upgradeReleaseTokens(ctx, &b); err != nil {
167+ return err
168+ }
169+ }
160170 b.Put(key, mustMarshal(&storagepb.SchemaVersion{Version: target}))
161171 _, err := s.db.Apply(ctx, &b)
162172 if errors.Is(err, kv.ErrConflict) {
@@ -214,6 +224,17 @@ func (s *Store) upgradeForceMergeTokens(ctx context.Context, b *kv.Batch) error
214224 return s.upgradeTokenScopes(ctx, b, preForceMergeScopes, []domain.Scope{domain.ScopeMRForceMerge})
215225 }
216226
227+// preReleaseScopes are the twelve scopes of schema 5, before release:write.
228+var preReleaseScopes = append(slices.Clone(preForceMergeScopes), domain.ScopeMRForceMerge)
229+
230+// upgradeReleaseTokens adds to b the schema 6 upgrade of the tokens: a token
231+// that holds all of preReleaseScopes, limited to no repository and no branch
232+// prefix, had the owner rights, which AllScopes also needs release:write for,
233+// so it gets it. Restricted or partial tokens are left as they are.
234+func (s *Store) upgradeReleaseTokens(ctx context.Context, b *kv.Batch) error {
235+ return s.upgradeTokenScopes(ctx, b, preReleaseScopes, []domain.Scope{domain.ScopeReleaseWrite})
236+}
237+
217238 func (s *Store) upgradeTokenScopes(ctx context.Context, b *kv.Batch, required, addedScopes []domain.Scope) error {
218239 for key, item := range s.db.Snapshot(ctx).Prefix(ctx, []byte(tokenPrefix)) {
219240 pb := &storagepb.Token{}
@@ -40,8 +40,10 @@ import (
40 // if missing) to the stored tokens that held all nine original scopes with no40 // if missing) to the stored tokens that held all nine original scopes with no
41 // repository list and no branch prefix, so that they keep the owner rights (see41 // repository list and no branch prefix, so that they keep the owner rights (see
42 // upgradeTokens). Version 5 grants mr:force-merge to unrestricted tokens42 // upgradeTokens). Version 5 grants mr:force-merge to unrestricted tokens
43-// holding all eleven previous scopes, preserving their owner rights.43+// holding all eleven previous scopes, preserving their owner rights. Version 6
44-const SchemaVersion = 544+// adds releases and release:write; older builds cannot clean up their metadata
45+// or preserve the owner rights of upgraded tokens (see upgradeReleaseTokens).
46+const SchemaVersion = 6
45 47
46 const schemaKey = "meta/schema"48 const schemaKey = "meta/schema"
47 49
@@ -110,7 +112,7 @@ func (s *Store) Check(ctx context.Context) error {
110 func (s *Store) checkSchema(ctx context.Context) error {112 func (s *Store) checkSchema(ctx context.Context) error {
111 log := zerowrap.FromCtx(ctx)113 log := zerowrap.FromCtx(ctx)
112 key := []byte(schemaKey)114 key := []byte(schemaKey)
113- for range 4 {115+ for range 5 {
114 item, ok := s.db.Get(ctx, key)116 item, ok := s.db.Get(ctx, key)
115 if !ok {117 if !ok {
116 var b kv.Batch118 var b kv.Batch
@@ -142,8 +144,11 @@ func (s *Store) checkSchema(ctx context.Context) error {
142 // The tokens are upgraded in the same batch, so that a crash144 // The tokens are upgraded in the same batch, so that a crash
143 // leaves the old version and the upgrade runs again.145 // leaves the old version and the upgrade runs again.
144 target := uint32(SchemaVersion)146 target := uint32(SchemaVersion)
145- if sv.GetVersion() < 4 {147+ switch {
148+ case sv.GetVersion() < 4:
146 target = 4149 target = 4
150+ case sv.GetVersion() == 4:
151+ target = 5
147 }152 }
148 var b kv.Batch153 var b kv.Batch
149 b.RequireVersion(key, item.Version)154 b.RequireVersion(key, item.Version)
@@ -157,6 +162,11 @@ func (s *Store) checkSchema(ctx context.Context) error {
157 return err162 return err
158 }163 }
159 }164 }
165+ if sv.GetVersion() == 5 {
166+ if err := s.upgradeReleaseTokens(ctx, &b); err != nil {
167+ return err
168+ }
169+ }
160 b.Put(key, mustMarshal(&storagepb.SchemaVersion{Version: target}))170 b.Put(key, mustMarshal(&storagepb.SchemaVersion{Version: target}))
161 _, err := s.db.Apply(ctx, &b)171 _, err := s.db.Apply(ctx, &b)
162 if errors.Is(err, kv.ErrConflict) {172 if errors.Is(err, kv.ErrConflict) {
@@ -214,6 +224,17 @@ func (s *Store) upgradeForceMergeTokens(ctx context.Context, b *kv.Batch) error
214 return s.upgradeTokenScopes(ctx, b, preForceMergeScopes, []domain.Scope{domain.ScopeMRForceMerge})224 return s.upgradeTokenScopes(ctx, b, preForceMergeScopes, []domain.Scope{domain.ScopeMRForceMerge})
215 }225 }
216 226
227+// preReleaseScopes are the twelve scopes of schema 5, before release:write.
228+var preReleaseScopes = append(slices.Clone(preForceMergeScopes), domain.ScopeMRForceMerge)
229+
230+// upgradeReleaseTokens adds to b the schema 6 upgrade of the tokens: a token
231+// that holds all of preReleaseScopes, limited to no repository and no branch
232+// prefix, had the owner rights, which AllScopes also needs release:write for,
233+// so it gets it. Restricted or partial tokens are left as they are.
234+func (s *Store) upgradeReleaseTokens(ctx context.Context, b *kv.Batch) error {
235+ return s.upgradeTokenScopes(ctx, b, preReleaseScopes, []domain.Scope{domain.ScopeReleaseWrite})
236+}
237+
217 func (s *Store) upgradeTokenScopes(ctx context.Context, b *kv.Batch, required, addedScopes []domain.Scope) error {238 func (s *Store) upgradeTokenScopes(ctx context.Context, b *kv.Batch, required, addedScopes []domain.Scope) error {
218 for key, item := range s.db.Snapshot(ctx).Prefix(ctx, []byte(tokenPrefix)) {239 for key, item := range s.db.Snapshot(ctx).Prefix(ctx, []byte(tokenPrefix)) {
219 pb := &storagepb.Token{}240 pb := &storagepb.Token{}
M internal/adapters/kvstore/store_test.go +57 -0
@@ -363,3 +363,60 @@ func TestRepoWithoutTopicsReadsNil(t *testing.T) {
363363 require.NoError(t, err)
364364 assert.Equal(t, []string{"a", "b"}, got.Topics)
365365 }
366+
367+func TestSchema6TokenUpgrade(t *testing.T) {
368+ ctx := context.Background()
369+ for _, from := range []uint32{3, 4, 5} {
370+ t.Run(fmt.Sprint(from), func(t *testing.T) {
371+ dir := t.TempDir()
372+ putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: from}))
373+ all := domain.AllScopes()
374+ scopes := make([]string, 0, len(all)-1)
375+ for _, sc := range all {
376+ if sc != domain.ScopeReleaseWrite {
377+ scopes = append(scopes, sc.String())
378+ }
379+ }
380+ for _, tc := range []struct {
381+ id string
382+ scopes []string
383+ repos, prefixes []string
384+ upgraded bool
385+ }{
386+ {id: "full", scopes: scopes, upgraded: true},
387+ {id: "partial", scopes: []string{"repo:read"}},
388+ {id: "missing-review", scopes: func() []string {
389+ var out []string
390+ for _, s := range scopes {
391+ if s != "mr:review" {
392+ out = append(out, s)
393+ }
394+ }
395+ return out
396+ }(), upgraded: from < 4},
397+ {id: "repo", scopes: scopes, repos: []string{"a"}},
398+ {id: "branch", scopes: scopes, prefixes: []string{"bot/"}},
399+ } {
400+ putRaw(t, dir, "token/"+tc.id, mustProto(t, &storagepb.Token{Id: tc.id, Scopes: tc.scopes, Repos: tc.repos, BranchPrefixes: tc.prefixes}))
401+ }
402+ s, err := kvstore.Open(ctx, dir)
403+ require.NoError(t, err)
404+ require.NoError(t, s.Close(ctx))
405+ assert.Equal(t, uint32(kvstore.SchemaVersion), readSchema(t, dir))
406+ db, err := kv.Open(ctx, dir, kv.Options{})
407+ require.NoError(t, err)
408+ defer db.Close(ctx)
409+ for _, id := range []string{"full", "partial", "missing-review", "repo", "branch"} {
410+ item, ok := db.Get(ctx, []byte("token/"+id))
411+ require.True(t, ok)
412+ var pb storagepb.Token
413+ require.NoError(t, proto.Unmarshal(item.Value, &pb))
414+ if id == "full" || (id == "missing-review" && from < 4) {
415+ assert.Contains(t, pb.Scopes, "release:write")
416+ } else {
417+ assert.NotContains(t, pb.Scopes, "release:write")
418+ }
419+ }
420+ })
421+ }
422+}
@@ -363,3 +363,60 @@ func TestRepoWithoutTopicsReadsNil(t *testing.T) {
363 require.NoError(t, err)363 require.NoError(t, err)
364 assert.Equal(t, []string{"a", "b"}, got.Topics)364 assert.Equal(t, []string{"a", "b"}, got.Topics)
365 }365 }
366+
367+func TestSchema6TokenUpgrade(t *testing.T) {
368+ ctx := context.Background()
369+ for _, from := range []uint32{3, 4, 5} {
370+ t.Run(fmt.Sprint(from), func(t *testing.T) {
371+ dir := t.TempDir()
372+ putRaw(t, dir, "meta/schema", mustProto(t, &storagepb.SchemaVersion{Version: from}))
373+ all := domain.AllScopes()
374+ scopes := make([]string, 0, len(all)-1)
375+ for _, sc := range all {
376+ if sc != domain.ScopeReleaseWrite {
377+ scopes = append(scopes, sc.String())
378+ }
379+ }
380+ for _, tc := range []struct {
381+ id string
382+ scopes []string
383+ repos, prefixes []string
384+ upgraded bool
385+ }{
386+ {id: "full", scopes: scopes, upgraded: true},
387+ {id: "partial", scopes: []string{"repo:read"}},
388+ {id: "missing-review", scopes: func() []string {
389+ var out []string
390+ for _, s := range scopes {
391+ if s != "mr:review" {
392+ out = append(out, s)
393+ }
394+ }
395+ return out
396+ }(), upgraded: from < 4},
397+ {id: "repo", scopes: scopes, repos: []string{"a"}},
398+ {id: "branch", scopes: scopes, prefixes: []string{"bot/"}},
399+ } {
400+ putRaw(t, dir, "token/"+tc.id, mustProto(t, &storagepb.Token{Id: tc.id, Scopes: tc.scopes, Repos: tc.repos, BranchPrefixes: tc.prefixes}))
401+ }
402+ s, err := kvstore.Open(ctx, dir)
403+ require.NoError(t, err)
404+ require.NoError(t, s.Close(ctx))
405+ assert.Equal(t, uint32(kvstore.SchemaVersion), readSchema(t, dir))
406+ db, err := kv.Open(ctx, dir, kv.Options{})
407+ require.NoError(t, err)
408+ defer db.Close(ctx)
409+ for _, id := range []string{"full", "partial", "missing-review", "repo", "branch"} {
410+ item, ok := db.Get(ctx, []byte("token/"+id))
411+ require.True(t, ok)
412+ var pb storagepb.Token
413+ require.NoError(t, proto.Unmarshal(item.Value, &pb))
414+ if id == "full" || (id == "missing-review" && from < 4) {
415+ assert.Contains(t, pb.Scopes, "release:write")
416+ } else {
417+ assert.NotContains(t, pb.Scopes, "release:write")
418+ }
419+ }
420+ })
421+ }
422+}
M internal/domain/event.go +44 -0
@@ -366,3 +366,47 @@ type MirrorRecovered struct {
366366 }
367367
368368 func (MirrorRecovered) EventKind() string { return "mirror.recovered" }
369+
370+// ReleasePublished records a release lifecycle change.
371+type ReleasePublished struct {
372+ Release Release
373+ At time.Time
374+ Visibility Visibility
375+ CreatedAt time.Time
376+}
377+
378+// EventKind implements Event.
379+func (ReleasePublished) EventKind() string { return "release.published" }
380+
381+// ReleaseUpdated records a release lifecycle change.
382+type ReleaseUpdated struct {
383+ Release Release
384+ At time.Time
385+ Visibility Visibility
386+ CreatedAt time.Time
387+}
388+
389+// EventKind implements Event.
390+func (ReleaseUpdated) EventKind() string { return "release.updated" }
391+
392+// ReleaseDeleted records a release lifecycle change.
393+type ReleaseDeleted struct {
394+ Release Release
395+ At time.Time
396+ Visibility Visibility
397+ CreatedAt time.Time
398+}
399+
400+// EventKind implements Event.
401+func (ReleaseDeleted) EventKind() string { return "release.deleted" }
402+
403+// ReleaseUnpublished records a release lifecycle change.
404+type ReleaseUnpublished struct {
405+ Release Release
406+ At time.Time
407+ Visibility Visibility
408+ CreatedAt time.Time
409+}
410+
411+// EventKind implements Event.
412+func (ReleaseUnpublished) EventKind() string { return "release.unpublished" }
@@ -366,3 +366,47 @@ type MirrorRecovered struct {
366 }366 }
367 367
368 func (MirrorRecovered) EventKind() string { return "mirror.recovered" }368 func (MirrorRecovered) EventKind() string { return "mirror.recovered" }
369+
370+// ReleasePublished records a release lifecycle change.
371+type ReleasePublished struct {
372+ Release Release
373+ At time.Time
374+ Visibility Visibility
375+ CreatedAt time.Time
376+}
377+
378+// EventKind implements Event.
379+func (ReleasePublished) EventKind() string { return "release.published" }
380+
381+// ReleaseUpdated records a release lifecycle change.
382+type ReleaseUpdated struct {
383+ Release Release
384+ At time.Time
385+ Visibility Visibility
386+ CreatedAt time.Time
387+}
388+
389+// EventKind implements Event.
390+func (ReleaseUpdated) EventKind() string { return "release.updated" }
391+
392+// ReleaseDeleted records a release lifecycle change.
393+type ReleaseDeleted struct {
394+ Release Release
395+ At time.Time
396+ Visibility Visibility
397+ CreatedAt time.Time
398+}
399+
400+// EventKind implements Event.
401+func (ReleaseDeleted) EventKind() string { return "release.deleted" }
402+
403+// ReleaseUnpublished records a release lifecycle change.
404+type ReleaseUnpublished struct {
405+ Release Release
406+ At time.Time
407+ Visibility Visibility
408+ CreatedAt time.Time
409+}
410+
411+// EventKind implements Event.
412+func (ReleaseUnpublished) EventKind() string { return "release.unpublished" }
A internal/domain/release.go +165 -0
@@ -0,0 +1,165 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain
4+
5+import (
6+ "fmt"
7+ "mime"
8+ "strings"
9+ "time"
10+ "unicode"
11+ "unicode/utf8"
12+
13+ "golang.org/x/text/unicode/norm"
14+)
15+
16+// Release binds release notes and assets to a repository tag.
17+type Release struct {
18+ ID int64
19+ Repo RepoName
20+ Tag, Title, Notes string
21+ Draft, Prerelease, MakeLatest bool
22+ // TargetCommitish is the commit at publication, empty for an unresolved draft.
23+ TargetCommitish string
24+ Author string
25+ CreatedAt, PublishedAt, UpdatedAt time.Time
26+ Version uint64
27+}
28+
29+// ReleaseAssetState distinguishes reserved uploads from visible assets.
30+type ReleaseAssetState string
31+
32+const (
33+ ReleaseAssetUploading ReleaseAssetState = "uploading"
34+ ReleaseAssetUploaded ReleaseAssetState = "uploaded"
35+)
36+
37+// ReleaseAsset is metadata only; its contents live in the blob store.
38+type ReleaseAsset struct {
39+ ID, ReleaseID int64
40+ Repo RepoName
41+ Name, Label, ContentType string
42+ Size int64
43+ Digest string
44+ State ReleaseAssetState
45+ Uploader string
46+ // Charged counts bytes reserved against repository quota.
47+ Charged int64
48+ // MultipartID identifies an unfinished upload for cleanup.
49+ MultipartID string
50+ CreatedAt, UpdatedAt time.Time
51+ Version uint64
52+}
53+
54+// BlobKey contains only a globally allocated identifier.
55+func (a ReleaseAsset) BlobKey() string { return fmt.Sprintf("release/%020d", a.ID) }
56+
57+const (
58+ ReleaseMaxTitleLen = 256
59+ ReleaseMaxNotesLen = 125_000
60+ ReleaseAssetMaxNameLen = 255
61+ ReleaseAssetMaxLabelLen = 255
62+ ReleaseAssetPendingMax = 24 * time.Hour
63+ DefaultReleaseAssetMax = 2 << 30
64+ DefaultReleaseAssetsMax = 1000
65+ DefaultReleaseQuota = 20 << 30
66+)
67+
68+// ParseReleaseTag validates a short tag using Git reference-name rules.
69+func ParseReleaseTag(s string) (string, error) {
70+ if s == "" {
71+ return "", fmt.Errorf("%w: empty tag name", ErrInvalid)
72+ }
73+ if strings.HasPrefix(s, "refs/") {
74+ return "", fmt.Errorf("%w: %q: give the short tag name", ErrInvalid, s)
75+ }
76+ if reason := refNameProblem(s); reason != "" {
77+ return "", fmt.Errorf("%w: %q: %s", ErrInvalid, s, reason)
78+ }
79+ return s, nil
80+}
81+
82+func cleanReleaseText(s string, limit int, multiline bool) (string, error) {
83+ if !utf8.ValidString(s) {
84+ return "", fmt.Errorf("%w: invalid UTF-8", ErrInvalid)
85+ }
86+ s = norm.NFC.String(s)
87+ s = strings.Map(func(r rune) rune {
88+ if multiline && (r == '\n' || r == '\t' || r == '\r') {
89+ return r
90+ }
91+ if unicode.IsControl(r) || (!multiline && isBidiControl(r)) {
92+ return -1
93+ }
94+ return r
95+ }, s)
96+ if utf8.RuneCountInString(s) > limit {
97+ return "", fmt.Errorf("%w: text exceeds %d characters", ErrInvalid, limit)
98+ }
99+ return s, nil
100+}
101+
102+// ParseReleaseTitle normalizes and cleans an optional title.
103+func ParseReleaseTitle(s string) (string, error) {
104+ return cleanReleaseText(s, ReleaseMaxTitleLen, false)
105+}
106+
107+// ParseReleaseNotes validates and cleans multiline notes.
108+func ParseReleaseNotes(s string) (string, error) {
109+ return cleanReleaseText(s, ReleaseMaxNotesLen, true)
110+}
111+
112+// ParseReleaseAssetLabel normalizes and cleans an optional label.
113+func ParseReleaseAssetLabel(s string) (string, error) {
114+ return cleanReleaseText(s, ReleaseAssetMaxLabelLen, false)
115+}
116+
117+// ParseReleaseAssetName validates a single filename without rewriting it.
118+func ParseReleaseAssetName(s string) (string, error) {
119+ if !utf8.ValidString(s) || s == "" || s == "." || s == ".." || utf8.RuneCountInString(s) > ReleaseAssetMaxNameLen || strings.ContainsAny(s, "/\\") {
120+ return "", fmt.Errorf("%w: invalid asset name", ErrInvalid)
121+ }
122+ for _, r := range s {
123+ if unicode.IsControl(r) {
124+ return "", fmt.Errorf("%w: control character in asset name", ErrInvalid)
125+ }
126+ }
127+ return s, nil
128+}
129+
130+// ParseReleaseContentType validates a printable ASCII media type.
131+func ParseReleaseContentType(s string) (string, error) {
132+ if s == "" {
133+ return "application/octet-stream", nil
134+ }
135+ if len(s) > 255 {
136+ return "", fmt.Errorf("%w: media type too long", ErrInvalid)
137+ }
138+ for _, r := range s {
139+ if r < 32 || r > 126 {
140+ return "", fmt.Errorf("%w: invalid media type", ErrInvalid)
141+ }
142+ }
143+ if _, _, err := mime.ParseMediaType(s); err != nil {
144+ return "", fmt.Errorf("%w: media type: %v", ErrInvalid, err)
145+ }
146+ return s, nil
147+}
148+
149+// Published reports whether the release is public to repository readers.
150+func (r Release) Published() bool { return !r.Draft }
151+
152+// LatestRelease selects a pin first, then the newest stable published release.
153+func LatestRelease(rs []Release) (Release, bool) {
154+ var best Release
155+ ok := false
156+ for _, r := range rs {
157+ if r.Draft || r.Prerelease {
158+ continue
159+ }
160+ if !ok || (r.MakeLatest && !best.MakeLatest) || (r.MakeLatest == best.MakeLatest && (r.CreatedAt.After(best.CreatedAt) || (r.CreatedAt.Equal(best.CreatedAt) && r.ID > best.ID))) {
161+ best, ok = r, true
162+ }
163+ }
164+ return best, ok
165+}
@@ -0,0 +1,165 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain
4+
5+import (
6+ "fmt"
7+ "mime"
8+ "strings"
9+ "time"
10+ "unicode"
11+ "unicode/utf8"
12+
13+ "golang.org/x/text/unicode/norm"
14+)
15+
16+// Release binds release notes and assets to a repository tag.
17+type Release struct {
18+ ID int64
19+ Repo RepoName
20+ Tag, Title, Notes string
21+ Draft, Prerelease, MakeLatest bool
22+ // TargetCommitish is the commit at publication, empty for an unresolved draft.
23+ TargetCommitish string
24+ Author string
25+ CreatedAt, PublishedAt, UpdatedAt time.Time
26+ Version uint64
27+}
28+
29+// ReleaseAssetState distinguishes reserved uploads from visible assets.
30+type ReleaseAssetState string
31+
32+const (
33+ ReleaseAssetUploading ReleaseAssetState = "uploading"
34+ ReleaseAssetUploaded ReleaseAssetState = "uploaded"
35+)
36+
37+// ReleaseAsset is metadata only; its contents live in the blob store.
38+type ReleaseAsset struct {
39+ ID, ReleaseID int64
40+ Repo RepoName
41+ Name, Label, ContentType string
42+ Size int64
43+ Digest string
44+ State ReleaseAssetState
45+ Uploader string
46+ // Charged counts bytes reserved against repository quota.
47+ Charged int64
48+ // MultipartID identifies an unfinished upload for cleanup.
49+ MultipartID string
50+ CreatedAt, UpdatedAt time.Time
51+ Version uint64
52+}
53+
54+// BlobKey contains only a globally allocated identifier.
55+func (a ReleaseAsset) BlobKey() string { return fmt.Sprintf("release/%020d", a.ID) }
56+
57+const (
58+ ReleaseMaxTitleLen = 256
59+ ReleaseMaxNotesLen = 125_000
60+ ReleaseAssetMaxNameLen = 255
61+ ReleaseAssetMaxLabelLen = 255
62+ ReleaseAssetPendingMax = 24 * time.Hour
63+ DefaultReleaseAssetMax = 2 << 30
64+ DefaultReleaseAssetsMax = 1000
65+ DefaultReleaseQuota = 20 << 30
66+)
67+
68+// ParseReleaseTag validates a short tag using Git reference-name rules.
69+func ParseReleaseTag(s string) (string, error) {
70+ if s == "" {
71+ return "", fmt.Errorf("%w: empty tag name", ErrInvalid)
72+ }
73+ if strings.HasPrefix(s, "refs/") {
74+ return "", fmt.Errorf("%w: %q: give the short tag name", ErrInvalid, s)
75+ }
76+ if reason := refNameProblem(s); reason != "" {
77+ return "", fmt.Errorf("%w: %q: %s", ErrInvalid, s, reason)
78+ }
79+ return s, nil
80+}
81+
82+func cleanReleaseText(s string, limit int, multiline bool) (string, error) {
83+ if !utf8.ValidString(s) {
84+ return "", fmt.Errorf("%w: invalid UTF-8", ErrInvalid)
85+ }
86+ s = norm.NFC.String(s)
87+ s = strings.Map(func(r rune) rune {
88+ if multiline && (r == '\n' || r == '\t' || r == '\r') {
89+ return r
90+ }
91+ if unicode.IsControl(r) || (!multiline && isBidiControl(r)) {
92+ return -1
93+ }
94+ return r
95+ }, s)
96+ if utf8.RuneCountInString(s) > limit {
97+ return "", fmt.Errorf("%w: text exceeds %d characters", ErrInvalid, limit)
98+ }
99+ return s, nil
100+}
101+
102+// ParseReleaseTitle normalizes and cleans an optional title.
103+func ParseReleaseTitle(s string) (string, error) {
104+ return cleanReleaseText(s, ReleaseMaxTitleLen, false)
105+}
106+
107+// ParseReleaseNotes validates and cleans multiline notes.
108+func ParseReleaseNotes(s string) (string, error) {
109+ return cleanReleaseText(s, ReleaseMaxNotesLen, true)
110+}
111+
112+// ParseReleaseAssetLabel normalizes and cleans an optional label.
113+func ParseReleaseAssetLabel(s string) (string, error) {
114+ return cleanReleaseText(s, ReleaseAssetMaxLabelLen, false)
115+}
116+
117+// ParseReleaseAssetName validates a single filename without rewriting it.
118+func ParseReleaseAssetName(s string) (string, error) {
119+ if !utf8.ValidString(s) || s == "" || s == "." || s == ".." || utf8.RuneCountInString(s) > ReleaseAssetMaxNameLen || strings.ContainsAny(s, "/\\") {
120+ return "", fmt.Errorf("%w: invalid asset name", ErrInvalid)
121+ }
122+ for _, r := range s {
123+ if unicode.IsControl(r) {
124+ return "", fmt.Errorf("%w: control character in asset name", ErrInvalid)
125+ }
126+ }
127+ return s, nil
128+}
129+
130+// ParseReleaseContentType validates a printable ASCII media type.
131+func ParseReleaseContentType(s string) (string, error) {
132+ if s == "" {
133+ return "application/octet-stream", nil
134+ }
135+ if len(s) > 255 {
136+ return "", fmt.Errorf("%w: media type too long", ErrInvalid)
137+ }
138+ for _, r := range s {
139+ if r < 32 || r > 126 {
140+ return "", fmt.Errorf("%w: invalid media type", ErrInvalid)
141+ }
142+ }
143+ if _, _, err := mime.ParseMediaType(s); err != nil {
144+ return "", fmt.Errorf("%w: media type: %v", ErrInvalid, err)
145+ }
146+ return s, nil
147+}
148+
149+// Published reports whether the release is public to repository readers.
150+func (r Release) Published() bool { return !r.Draft }
151+
152+// LatestRelease selects a pin first, then the newest stable published release.
153+func LatestRelease(rs []Release) (Release, bool) {
154+ var best Release
155+ ok := false
156+ for _, r := range rs {
157+ if r.Draft || r.Prerelease {
158+ continue
159+ }
160+ if !ok || (r.MakeLatest && !best.MakeLatest) || (r.MakeLatest == best.MakeLatest && (r.CreatedAt.After(best.CreatedAt) || (r.CreatedAt.Equal(best.CreatedAt) && r.ID > best.ID))) {
161+ best, ok = r, true
162+ }
163+ }
164+ return best, ok
165+}
A internal/domain/release_test.go +59 -0
@@ -0,0 +1,59 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+package domain_test
3+
4+import (
5+ "bnema.dev/ermite/internal/domain"
6+ "github.com/stretchr/testify/require"
7+ "strings"
8+ "testing"
9+ "time"
10+)
11+
12+func TestReleaseParsers(t *testing.T) {
13+ for _, tc := range []struct {
14+ parse func(string) (string, error)
15+ good, bad []string
16+ }{
17+ {domain.ParseReleaseTag, []string{"v1.0", "nightly/x"}, []string{"", "refs/tags/v1", "..", "a..b", "a b"}},
18+ {domain.ParseReleaseTitle, []string{"", strings.Repeat("é", 256)}, []string{strings.Repeat("x", 257), "\xff"}},
19+ {domain.ParseReleaseNotes, []string{"", "a\n\t\rb", strings.Repeat("x", 125000)}, []string{"\xff", strings.Repeat("x", 125001)}},
20+ {domain.ParseReleaseAssetName, []string{"a b", "é", strings.Repeat("x", 255)}, []string{"", ".", "..", "a/b", "a\\b", "a\x00", "\xff", strings.Repeat("x", 256)}},
21+ {domain.ParseReleaseAssetLabel, []string{"", strings.Repeat("x", 255)}, []string{"\xff", strings.Repeat("x", 256)}},
22+ {domain.ParseReleaseContentType, []string{"", "text/plain; charset=utf-8"}, []string{"bad/type/type", "text/plain\n", strings.Repeat("x", 256), "é"}},
23+ } {
24+ for _, s := range tc.good {
25+ _, err := tc.parse(s)
26+ require.NoError(t, err, s)
27+ }
28+ for _, s := range tc.bad {
29+ _, err := tc.parse(s)
30+ require.ErrorIs(t, err, domain.ErrInvalid, s)
31+ }
32+ }
33+ title, err := domain.ParseReleaseTitle("e\u0301\x00\u202e")
34+ require.NoError(t, err)
35+ require.Equal(t, "é", title)
36+ notes, err := domain.ParseReleaseNotes("a\x00\n\t\r")
37+ require.NoError(t, err)
38+ require.Equal(t, "a\n\t\r", notes)
39+ require.Equal(t, "release/00000000000000000001", (domain.ReleaseAsset{ID: 1}).BlobKey())
40+ scope, err := domain.ParseScope("release:write")
41+ require.NoError(t, err)
42+ require.Equal(t, domain.ScopeReleaseWrite, scope)
43+}
44+func TestLatestRelease(t *testing.T) {
45+ now := time.Now()
46+ rs := []domain.Release{{ID: 1, CreatedAt: now, MakeLatest: true}, {ID: 2, CreatedAt: now.Add(time.Hour)}, {ID: 3, Draft: true, MakeLatest: true}, {ID: 4, Prerelease: true, MakeLatest: true}}
47+ r, ok := domain.LatestRelease(rs)
48+ require.True(t, ok)
49+ require.EqualValues(t, 1, r.ID)
50+ rs[0].MakeLatest = false
51+ r, ok = domain.LatestRelease(rs)
52+ require.True(t, ok)
53+ require.EqualValues(t, 2, r.ID)
54+ rs[0].CreatedAt = rs[1].CreatedAt
55+ r, _ = domain.LatestRelease(rs)
56+ require.EqualValues(t, 2, r.ID)
57+ _, ok = domain.LatestRelease(rs[2:])
58+ require.False(t, ok)
59+}
@@ -0,0 +1,59 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+package domain_test
3+
4+import (
5+ "bnema.dev/ermite/internal/domain"
6+ "github.com/stretchr/testify/require"
7+ "strings"
8+ "testing"
9+ "time"
10+)
11+
12+func TestReleaseParsers(t *testing.T) {
13+ for _, tc := range []struct {
14+ parse func(string) (string, error)
15+ good, bad []string
16+ }{
17+ {domain.ParseReleaseTag, []string{"v1.0", "nightly/x"}, []string{"", "refs/tags/v1", "..", "a..b", "a b"}},
18+ {domain.ParseReleaseTitle, []string{"", strings.Repeat("é", 256)}, []string{strings.Repeat("x", 257), "\xff"}},
19+ {domain.ParseReleaseNotes, []string{"", "a\n\t\rb", strings.Repeat("x", 125000)}, []string{"\xff", strings.Repeat("x", 125001)}},
20+ {domain.ParseReleaseAssetName, []string{"a b", "é", strings.Repeat("x", 255)}, []string{"", ".", "..", "a/b", "a\\b", "a\x00", "\xff", strings.Repeat("x", 256)}},
21+ {domain.ParseReleaseAssetLabel, []string{"", strings.Repeat("x", 255)}, []string{"\xff", strings.Repeat("x", 256)}},
22+ {domain.ParseReleaseContentType, []string{"", "text/plain; charset=utf-8"}, []string{"bad/type/type", "text/plain\n", strings.Repeat("x", 256), "é"}},
23+ } {
24+ for _, s := range tc.good {
25+ _, err := tc.parse(s)
26+ require.NoError(t, err, s)
27+ }
28+ for _, s := range tc.bad {
29+ _, err := tc.parse(s)
30+ require.ErrorIs(t, err, domain.ErrInvalid, s)
31+ }
32+ }
33+ title, err := domain.ParseReleaseTitle("e\u0301\x00\u202e")
34+ require.NoError(t, err)
35+ require.Equal(t, "é", title)
36+ notes, err := domain.ParseReleaseNotes("a\x00\n\t\r")
37+ require.NoError(t, err)
38+ require.Equal(t, "a\n\t\r", notes)
39+ require.Equal(t, "release/00000000000000000001", (domain.ReleaseAsset{ID: 1}).BlobKey())
40+ scope, err := domain.ParseScope("release:write")
41+ require.NoError(t, err)
42+ require.Equal(t, domain.ScopeReleaseWrite, scope)
43+}
44+func TestLatestRelease(t *testing.T) {
45+ now := time.Now()
46+ rs := []domain.Release{{ID: 1, CreatedAt: now, MakeLatest: true}, {ID: 2, CreatedAt: now.Add(time.Hour)}, {ID: 3, Draft: true, MakeLatest: true}, {ID: 4, Prerelease: true, MakeLatest: true}}
47+ r, ok := domain.LatestRelease(rs)
48+ require.True(t, ok)
49+ require.EqualValues(t, 1, r.ID)
50+ rs[0].MakeLatest = false
51+ r, ok = domain.LatestRelease(rs)
52+ require.True(t, ok)
53+ require.EqualValues(t, 2, r.ID)
54+ rs[0].CreatedAt = rs[1].CreatedAt
55+ r, _ = domain.LatestRelease(rs)
56+ require.EqualValues(t, 2, r.ID)
57+ _, ok = domain.LatestRelease(rs[2:])
58+ require.False(t, ok)
59+}
M internal/domain/token.go +4 -2
@@ -45,16 +45,18 @@ const (
4545 // ScopeIssueWrite creates, comments on, closes and reopens issues. Moderation
4646 // is not part of it: it stays with the owner.
4747 ScopeIssueWrite Scope = "issue:write"
48+ // ScopeReleaseWrite creates, edits and deletes releases and their assets.
49+ ScopeReleaseWrite Scope = "release:write"
4850 )
4951
50-// AllScopes returns the twelve scopes in a stable order.
52+// AllScopes returns the thirteen scopes in a stable order.
5153 func AllScopes() []Scope {
5254 return []Scope{
5355 ScopeRepoRead, ScopeRepoWrite, ScopeRepoAdmin,
5456 ScopeMRWrite, ScopeMRMerge, ScopeMRForceMerge, ScopeMRReview,
5557 ScopeCIWrite, ScopeSecretsWrite, ScopeRunnerRegister,
5658 ScopeTokenAdmin,
57- ScopeIssueWrite,
59+ ScopeIssueWrite, ScopeReleaseWrite,
5860 }
5961 }
6062
@@ -45,16 +45,18 @@ const (
45 // ScopeIssueWrite creates, comments on, closes and reopens issues. Moderation45 // ScopeIssueWrite creates, comments on, closes and reopens issues. Moderation
46 // is not part of it: it stays with the owner.46 // is not part of it: it stays with the owner.
47 ScopeIssueWrite Scope = "issue:write"47 ScopeIssueWrite Scope = "issue:write"
48+ // ScopeReleaseWrite creates, edits and deletes releases and their assets.
49+ ScopeReleaseWrite Scope = "release:write"
48 )50 )
49 51
50-// AllScopes returns the twelve scopes in a stable order.52+// AllScopes returns the thirteen scopes in a stable order.
51 func AllScopes() []Scope {53 func AllScopes() []Scope {
52 return []Scope{54 return []Scope{
53 ScopeRepoRead, ScopeRepoWrite, ScopeRepoAdmin,55 ScopeRepoRead, ScopeRepoWrite, ScopeRepoAdmin,
54 ScopeMRWrite, ScopeMRMerge, ScopeMRForceMerge, ScopeMRReview,56 ScopeMRWrite, ScopeMRMerge, ScopeMRForceMerge, ScopeMRReview,
55 ScopeCIWrite, ScopeSecretsWrite, ScopeRunnerRegister,57 ScopeCIWrite, ScopeSecretsWrite, ScopeRunnerRegister,
56 ScopeTokenAdmin,58 ScopeTokenAdmin,
57- ScopeIssueWrite,59+ ScopeIssueWrite, ScopeReleaseWrite,
58 }60 }
59 }61 }
60 62
M internal/domain/token_test.go +5 -1
@@ -16,7 +16,11 @@ import (
1616 func TestScopes(t *testing.T) {
1717 t.Parallel()
1818 all := domain.AllScopes()
19- assert.Len(t, all, 12)
19+ assert.Len(t, all, 13)
20+ assert.Equal(t, domain.ScopeMRForceMerge, all[5])
21+ assert.Equal(t, domain.ScopeReleaseWrite, all[len(all)-1])
22+ assert.Contains(t, all, domain.ScopeReleaseWrite)
23+ assert.False(t, domain.TokenPrincipal(domain.Token{Scopes: all[:len(all)-1]}).HasOwnerRights(), "missing only release:write")
2024 assert.Contains(t, all, domain.ScopeIssueWrite)
2125 got, err := domain.ParseScope("issue:write")
2226 require.NoError(t, err)
@@ -16,7 +16,11 @@ import (
16 func TestScopes(t *testing.T) {16 func TestScopes(t *testing.T) {
17 t.Parallel()17 t.Parallel()
18 all := domain.AllScopes()18 all := domain.AllScopes()
19- assert.Len(t, all, 12)19+ assert.Len(t, all, 13)
20+ assert.Equal(t, domain.ScopeMRForceMerge, all[5])
21+ assert.Equal(t, domain.ScopeReleaseWrite, all[len(all)-1])
22+ assert.Contains(t, all, domain.ScopeReleaseWrite)
23+ assert.False(t, domain.TokenPrincipal(domain.Token{Scopes: all[:len(all)-1]}).HasOwnerRights(), "missing only release:write")
20 assert.Contains(t, all, domain.ScopeIssueWrite)24 assert.Contains(t, all, domain.ScopeIssueWrite)
21 got, err := domain.ParseScope("issue:write")25 got, err := domain.ParseScope("issue:write")
22 require.NoError(t, err)26 require.NoError(t, err)
A internal/ports/mocks/ReleaseRepoCleaner.go +168 -0
@@ -0,0 +1,168 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewReleaseRepoCleaner creates a new instance of ReleaseRepoCleaner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewReleaseRepoCleaner(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *ReleaseRepoCleaner {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &ReleaseRepoCleaner{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// ReleaseRepoCleaner is an autogenerated mock type for the ReleaseRepoCleaner type
38+type ReleaseRepoCleaner struct {
39+ mock.Mock
40+}
41+
42+type ReleaseRepoCleaner_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *ReleaseRepoCleaner) EXPECT() *ReleaseRepoCleaner_Expecter {
47+ return &ReleaseRepoCleaner_Expecter{mock: &_m.Mock}
48+}
49+
50+// AssetsOf provides a mock function for the type ReleaseRepoCleaner
51+func (_mock *ReleaseRepoCleaner) AssetsOf(ctx context.Context, repo domain.RepoName) ([]domain.ReleaseAsset, error) {
52+ ret := _mock.Called(ctx, repo)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for AssetsOf")
56+ }
57+
58+ var r0 []domain.ReleaseAsset
59+ var r1 error
60+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) ([]domain.ReleaseAsset, error)); ok {
61+ return returnFunc(ctx, repo)
62+ }
63+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) []domain.ReleaseAsset); ok {
64+ r0 = returnFunc(ctx, repo)
65+ } else {
66+ if ret.Get(0) != nil {
67+ r0 = ret.Get(0).([]domain.ReleaseAsset)
68+ }
69+ }
70+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) error); ok {
71+ r1 = returnFunc(ctx, repo)
72+ } else {
73+ r1 = ret.Error(1)
74+ }
75+ return r0, r1
76+}
77+
78+// ReleaseRepoCleaner_AssetsOf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AssetsOf'
79+type ReleaseRepoCleaner_AssetsOf_Call struct {
80+ *mock.Call
81+}
82+
83+// AssetsOf is a helper method to define mock.On call
84+// - ctx context.Context
85+// - repo domain.RepoName
86+func (_e *ReleaseRepoCleaner_Expecter) AssetsOf(ctx any, repo any) *ReleaseRepoCleaner_AssetsOf_Call {
87+ return &ReleaseRepoCleaner_AssetsOf_Call{Call: _e.mock.On("AssetsOf", ctx, repo)}
88+}
89+
90+func (_c *ReleaseRepoCleaner_AssetsOf_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *ReleaseRepoCleaner_AssetsOf_Call {
91+ _c.Call.Run(func(args mock.Arguments) {
92+ var arg0 context.Context
93+ if args[0] != nil {
94+ arg0 = args[0].(context.Context)
95+ }
96+ var arg1 domain.RepoName
97+ if args[1] != nil {
98+ arg1 = args[1].(domain.RepoName)
99+ }
100+ run(
101+ arg0,
102+ arg1,
103+ )
104+ })
105+ return _c
106+}
107+
108+func (_c *ReleaseRepoCleaner_AssetsOf_Call) Return(releaseAssets []domain.ReleaseAsset, err error) *ReleaseRepoCleaner_AssetsOf_Call {
109+ _c.Call.Return(releaseAssets, err)
110+ return _c
111+}
112+
113+func (_c *ReleaseRepoCleaner_AssetsOf_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) ([]domain.ReleaseAsset, error)) *ReleaseRepoCleaner_AssetsOf_Call {
114+ _c.Call.Return(run)
115+ return _c
116+}
117+
118+// OnRepoDeleted provides a mock function for the type ReleaseRepoCleaner
119+func (_mock *ReleaseRepoCleaner) OnRepoDeleted(ctx context.Context, repo domain.RepoName, assets []domain.ReleaseAsset) {
120+ _mock.Called(ctx, repo, assets)
121+ return
122+}
123+
124+// ReleaseRepoCleaner_OnRepoDeleted_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnRepoDeleted'
125+type ReleaseRepoCleaner_OnRepoDeleted_Call struct {
126+ *mock.Call
127+}
128+
129+// OnRepoDeleted is a helper method to define mock.On call
130+// - ctx context.Context
131+// - repo domain.RepoName
132+// - assets []domain.ReleaseAsset
133+func (_e *ReleaseRepoCleaner_Expecter) OnRepoDeleted(ctx any, repo any, assets any) *ReleaseRepoCleaner_OnRepoDeleted_Call {
134+ return &ReleaseRepoCleaner_OnRepoDeleted_Call{Call: _e.mock.On("OnRepoDeleted", ctx, repo, assets)}
135+}
136+
137+func (_c *ReleaseRepoCleaner_OnRepoDeleted_Call) Run(run func(ctx context.Context, repo domain.RepoName, assets []domain.ReleaseAsset)) *ReleaseRepoCleaner_OnRepoDeleted_Call {
138+ _c.Call.Run(func(args mock.Arguments) {
139+ var arg0 context.Context
140+ if args[0] != nil {
141+ arg0 = args[0].(context.Context)
142+ }
143+ var arg1 domain.RepoName
144+ if args[1] != nil {
145+ arg1 = args[1].(domain.RepoName)
146+ }
147+ var arg2 []domain.ReleaseAsset
148+ if args[2] != nil {
149+ arg2 = args[2].([]domain.ReleaseAsset)
150+ }
151+ run(
152+ arg0,
153+ arg1,
154+ arg2,
155+ )
156+ })
157+ return _c
158+}
159+
160+func (_c *ReleaseRepoCleaner_OnRepoDeleted_Call) Return() *ReleaseRepoCleaner_OnRepoDeleted_Call {
161+ _c.Call.Return()
162+ return _c
163+}
164+
165+func (_c *ReleaseRepoCleaner_OnRepoDeleted_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, assets []domain.ReleaseAsset)) *ReleaseRepoCleaner_OnRepoDeleted_Call {
166+ _c.Run(run)
167+ return _c
168+}
@@ -0,0 +1,168 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewReleaseRepoCleaner creates a new instance of ReleaseRepoCleaner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewReleaseRepoCleaner(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *ReleaseRepoCleaner {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &ReleaseRepoCleaner{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// ReleaseRepoCleaner is an autogenerated mock type for the ReleaseRepoCleaner type
38+type ReleaseRepoCleaner struct {
39+ mock.Mock
40+}
41+
42+type ReleaseRepoCleaner_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *ReleaseRepoCleaner) EXPECT() *ReleaseRepoCleaner_Expecter {
47+ return &ReleaseRepoCleaner_Expecter{mock: &_m.Mock}
48+}
49+
50+// AssetsOf provides a mock function for the type ReleaseRepoCleaner
51+func (_mock *ReleaseRepoCleaner) AssetsOf(ctx context.Context, repo domain.RepoName) ([]domain.ReleaseAsset, error) {
52+ ret := _mock.Called(ctx, repo)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for AssetsOf")
56+ }
57+
58+ var r0 []domain.ReleaseAsset
59+ var r1 error
60+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) ([]domain.ReleaseAsset, error)); ok {
61+ return returnFunc(ctx, repo)
62+ }
63+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) []domain.ReleaseAsset); ok {
64+ r0 = returnFunc(ctx, repo)
65+ } else {
66+ if ret.Get(0) != nil {
67+ r0 = ret.Get(0).([]domain.ReleaseAsset)
68+ }
69+ }
70+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) error); ok {
71+ r1 = returnFunc(ctx, repo)
72+ } else {
73+ r1 = ret.Error(1)
74+ }
75+ return r0, r1
76+}
77+
78+// ReleaseRepoCleaner_AssetsOf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AssetsOf'
79+type ReleaseRepoCleaner_AssetsOf_Call struct {
80+ *mock.Call
81+}
82+
83+// AssetsOf is a helper method to define mock.On call
84+// - ctx context.Context
85+// - repo domain.RepoName
86+func (_e *ReleaseRepoCleaner_Expecter) AssetsOf(ctx any, repo any) *ReleaseRepoCleaner_AssetsOf_Call {
87+ return &ReleaseRepoCleaner_AssetsOf_Call{Call: _e.mock.On("AssetsOf", ctx, repo)}
88+}
89+
90+func (_c *ReleaseRepoCleaner_AssetsOf_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *ReleaseRepoCleaner_AssetsOf_Call {
91+ _c.Call.Run(func(args mock.Arguments) {
92+ var arg0 context.Context
93+ if args[0] != nil {
94+ arg0 = args[0].(context.Context)
95+ }
96+ var arg1 domain.RepoName
97+ if args[1] != nil {
98+ arg1 = args[1].(domain.RepoName)
99+ }
100+ run(
101+ arg0,
102+ arg1,
103+ )
104+ })
105+ return _c
106+}
107+
108+func (_c *ReleaseRepoCleaner_AssetsOf_Call) Return(releaseAssets []domain.ReleaseAsset, err error) *ReleaseRepoCleaner_AssetsOf_Call {
109+ _c.Call.Return(releaseAssets, err)
110+ return _c
111+}
112+
113+func (_c *ReleaseRepoCleaner_AssetsOf_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) ([]domain.ReleaseAsset, error)) *ReleaseRepoCleaner_AssetsOf_Call {
114+ _c.Call.Return(run)
115+ return _c
116+}
117+
118+// OnRepoDeleted provides a mock function for the type ReleaseRepoCleaner
119+func (_mock *ReleaseRepoCleaner) OnRepoDeleted(ctx context.Context, repo domain.RepoName, assets []domain.ReleaseAsset) {
120+ _mock.Called(ctx, repo, assets)
121+ return
122+}
123+
124+// ReleaseRepoCleaner_OnRepoDeleted_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnRepoDeleted'
125+type ReleaseRepoCleaner_OnRepoDeleted_Call struct {
126+ *mock.Call
127+}
128+
129+// OnRepoDeleted is a helper method to define mock.On call
130+// - ctx context.Context
131+// - repo domain.RepoName
132+// - assets []domain.ReleaseAsset
133+func (_e *ReleaseRepoCleaner_Expecter) OnRepoDeleted(ctx any, repo any, assets any) *ReleaseRepoCleaner_OnRepoDeleted_Call {
134+ return &ReleaseRepoCleaner_OnRepoDeleted_Call{Call: _e.mock.On("OnRepoDeleted", ctx, repo, assets)}
135+}
136+
137+func (_c *ReleaseRepoCleaner_OnRepoDeleted_Call) Run(run func(ctx context.Context, repo domain.RepoName, assets []domain.ReleaseAsset)) *ReleaseRepoCleaner_OnRepoDeleted_Call {
138+ _c.Call.Run(func(args mock.Arguments) {
139+ var arg0 context.Context
140+ if args[0] != nil {
141+ arg0 = args[0].(context.Context)
142+ }
143+ var arg1 domain.RepoName
144+ if args[1] != nil {
145+ arg1 = args[1].(domain.RepoName)
146+ }
147+ var arg2 []domain.ReleaseAsset
148+ if args[2] != nil {
149+ arg2 = args[2].([]domain.ReleaseAsset)
150+ }
151+ run(
152+ arg0,
153+ arg1,
154+ arg2,
155+ )
156+ })
157+ return _c
158+}
159+
160+func (_c *ReleaseRepoCleaner_OnRepoDeleted_Call) Return() *ReleaseRepoCleaner_OnRepoDeleted_Call {
161+ _c.Call.Return()
162+ return _c
163+}
164+
165+func (_c *ReleaseRepoCleaner_OnRepoDeleted_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, assets []domain.ReleaseAsset)) *ReleaseRepoCleaner_OnRepoDeleted_Call {
166+ _c.Run(run)
167+ return _c
168+}
A internal/ports/mocks/ReleaseService.go +997 -0
@@ -0,0 +1,997 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+ "io"
10+
11+ "bnema.dev/ermite/internal/domain"
12+ "bnema.dev/ermite/internal/ports"
13+ mock "github.com/stretchr/testify/mock"
14+)
15+
16+// NewReleaseService creates a new instance of ReleaseService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
17+// The first argument is typically a *testing.T value.
18+func NewReleaseService(t interface {
19+ mock.TestingT
20+ Cleanup(func())
21+}) *ReleaseService {
22+ if helper, ok := t.(interface{ Helper() }); ok {
23+ helper.Helper()
24+ }
25+
26+ mock := &ReleaseService{}
27+ mock.Mock.Test(t)
28+
29+ t.Cleanup(func() {
30+ if helper, ok := t.(interface{ Helper() }); ok {
31+ helper.Helper()
32+ }
33+ mock.AssertExpectations(t)
34+ })
35+
36+ return mock
37+}
38+
39+// ReleaseService is an autogenerated mock type for the ReleaseService type
40+type ReleaseService struct {
41+ mock.Mock
42+}
43+
44+type ReleaseService_Expecter struct {
45+ mock *mock.Mock
46+}
47+
48+func (_m *ReleaseService) EXPECT() *ReleaseService_Expecter {
49+ return &ReleaseService_Expecter{mock: &_m.Mock}
50+}
51+
52+// CreateRelease provides a mock function for the type ReleaseService
53+func (_mock *ReleaseService) CreateRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.CreateReleaseParams) (domain.Release, error) {
54+ ret := _mock.Called(ctx, p, repo, params)
55+
56+ if len(ret) == 0 {
57+ panic("no return value specified for CreateRelease")
58+ }
59+
60+ var r0 domain.Release
61+ var r1 error
62+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.CreateReleaseParams) (domain.Release, error)); ok {
63+ return returnFunc(ctx, p, repo, params)
64+ }
65+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.CreateReleaseParams) domain.Release); ok {
66+ r0 = returnFunc(ctx, p, repo, params)
67+ } else {
68+ r0 = ret.Get(0).(domain.Release)
69+ }
70+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, ports.CreateReleaseParams) error); ok {
71+ r1 = returnFunc(ctx, p, repo, params)
72+ } else {
73+ r1 = ret.Error(1)
74+ }
75+ return r0, r1
76+}
77+
78+// ReleaseService_CreateRelease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateRelease'
79+type ReleaseService_CreateRelease_Call struct {
80+ *mock.Call
81+}
82+
83+// CreateRelease is a helper method to define mock.On call
84+// - ctx context.Context
85+// - p domain.Principal
86+// - repo domain.RepoName
87+// - params ports.CreateReleaseParams
88+func (_e *ReleaseService_Expecter) CreateRelease(ctx any, p any, repo any, params any) *ReleaseService_CreateRelease_Call {
89+ return &ReleaseService_CreateRelease_Call{Call: _e.mock.On("CreateRelease", ctx, p, repo, params)}
90+}
91+
92+func (_c *ReleaseService_CreateRelease_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.CreateReleaseParams)) *ReleaseService_CreateRelease_Call {
93+ _c.Call.Run(func(args mock.Arguments) {
94+ var arg0 context.Context
95+ if args[0] != nil {
96+ arg0 = args[0].(context.Context)
97+ }
98+ var arg1 domain.Principal
99+ if args[1] != nil {
100+ arg1 = args[1].(domain.Principal)
101+ }
102+ var arg2 domain.RepoName
103+ if args[2] != nil {
104+ arg2 = args[2].(domain.RepoName)
105+ }
106+ var arg3 ports.CreateReleaseParams
107+ if args[3] != nil {
108+ arg3 = args[3].(ports.CreateReleaseParams)
109+ }
110+ run(
111+ arg0,
112+ arg1,
113+ arg2,
114+ arg3,
115+ )
116+ })
117+ return _c
118+}
119+
120+func (_c *ReleaseService_CreateRelease_Call) Return(release domain.Release, err error) *ReleaseService_CreateRelease_Call {
121+ _c.Call.Return(release, err)
122+ return _c
123+}
124+
125+func (_c *ReleaseService_CreateRelease_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.CreateReleaseParams) (domain.Release, error)) *ReleaseService_CreateRelease_Call {
126+ _c.Call.Return(run)
127+ return _c
128+}
129+
130+// DeleteAsset provides a mock function for the type ReleaseService
131+func (_mock *ReleaseService) DeleteAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64) error {
132+ ret := _mock.Called(ctx, p, repo, assetID)
133+
134+ if len(ret) == 0 {
135+ panic("no return value specified for DeleteAsset")
136+ }
137+
138+ var r0 error
139+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) error); ok {
140+ r0 = returnFunc(ctx, p, repo, assetID)
141+ } else {
142+ r0 = ret.Error(0)
143+ }
144+ return r0
145+}
146+
147+// ReleaseService_DeleteAsset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAsset'
148+type ReleaseService_DeleteAsset_Call struct {
149+ *mock.Call
150+}
151+
152+// DeleteAsset is a helper method to define mock.On call
153+// - ctx context.Context
154+// - p domain.Principal
155+// - repo domain.RepoName
156+// - assetID int64
157+func (_e *ReleaseService_Expecter) DeleteAsset(ctx any, p any, repo any, assetID any) *ReleaseService_DeleteAsset_Call {
158+ return &ReleaseService_DeleteAsset_Call{Call: _e.mock.On("DeleteAsset", ctx, p, repo, assetID)}
159+}
160+
161+func (_c *ReleaseService_DeleteAsset_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64)) *ReleaseService_DeleteAsset_Call {
162+ _c.Call.Run(func(args mock.Arguments) {
163+ var arg0 context.Context
164+ if args[0] != nil {
165+ arg0 = args[0].(context.Context)
166+ }
167+ var arg1 domain.Principal
168+ if args[1] != nil {
169+ arg1 = args[1].(domain.Principal)
170+ }
171+ var arg2 domain.RepoName
172+ if args[2] != nil {
173+ arg2 = args[2].(domain.RepoName)
174+ }
175+ var arg3 int64
176+ if args[3] != nil {
177+ arg3 = args[3].(int64)
178+ }
179+ run(
180+ arg0,
181+ arg1,
182+ arg2,
183+ arg3,
184+ )
185+ })
186+ return _c
187+}
188+
189+func (_c *ReleaseService_DeleteAsset_Call) Return(err error) *ReleaseService_DeleteAsset_Call {
190+ _c.Call.Return(err)
191+ return _c
192+}
193+
194+func (_c *ReleaseService_DeleteAsset_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64) error) *ReleaseService_DeleteAsset_Call {
195+ _c.Call.Return(run)
196+ return _c
197+}
198+
199+// DeleteRelease provides a mock function for the type ReleaseService
200+func (_mock *ReleaseService) DeleteRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64) error {
201+ ret := _mock.Called(ctx, p, repo, id)
202+
203+ if len(ret) == 0 {
204+ panic("no return value specified for DeleteRelease")
205+ }
206+
207+ var r0 error
208+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) error); ok {
209+ r0 = returnFunc(ctx, p, repo, id)
210+ } else {
211+ r0 = ret.Error(0)
212+ }
213+ return r0
214+}
215+
216+// ReleaseService_DeleteRelease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteRelease'
217+type ReleaseService_DeleteRelease_Call struct {
218+ *mock.Call
219+}
220+
221+// DeleteRelease is a helper method to define mock.On call
222+// - ctx context.Context
223+// - p domain.Principal
224+// - repo domain.RepoName
225+// - id int64
226+func (_e *ReleaseService_Expecter) DeleteRelease(ctx any, p any, repo any, id any) *ReleaseService_DeleteRelease_Call {
227+ return &ReleaseService_DeleteRelease_Call{Call: _e.mock.On("DeleteRelease", ctx, p, repo, id)}
228+}
229+
230+func (_c *ReleaseService_DeleteRelease_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64)) *ReleaseService_DeleteRelease_Call {
231+ _c.Call.Run(func(args mock.Arguments) {
232+ var arg0 context.Context
233+ if args[0] != nil {
234+ arg0 = args[0].(context.Context)
235+ }
236+ var arg1 domain.Principal
237+ if args[1] != nil {
238+ arg1 = args[1].(domain.Principal)
239+ }
240+ var arg2 domain.RepoName
241+ if args[2] != nil {
242+ arg2 = args[2].(domain.RepoName)
243+ }
244+ var arg3 int64
245+ if args[3] != nil {
246+ arg3 = args[3].(int64)
247+ }
248+ run(
249+ arg0,
250+ arg1,
251+ arg2,
252+ arg3,
253+ )
254+ })
255+ return _c
256+}
257+
258+func (_c *ReleaseService_DeleteRelease_Call) Return(err error) *ReleaseService_DeleteRelease_Call {
259+ _c.Call.Return(err)
260+ return _c
261+}
262+
263+func (_c *ReleaseService_DeleteRelease_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64) error) *ReleaseService_DeleteRelease_Call {
264+ _c.Call.Return(run)
265+ return _c
266+}
267+
268+// DownloadAsset provides a mock function for the type ReleaseService
269+func (_mock *ReleaseService) DownloadAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.AssetRef, offset int64, length int64, send func(ports.AssetChunk) error) error {
270+ ret := _mock.Called(ctx, p, repo, ref, offset, length, send)
271+
272+ if len(ret) == 0 {
273+ panic("no return value specified for DownloadAsset")
274+ }
275+
276+ var r0 error
277+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.AssetRef, int64, int64, func(ports.AssetChunk) error) error); ok {
278+ r0 = returnFunc(ctx, p, repo, ref, offset, length, send)
279+ } else {
280+ r0 = ret.Error(0)
281+ }
282+ return r0
283+}
284+
285+// ReleaseService_DownloadAsset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DownloadAsset'
286+type ReleaseService_DownloadAsset_Call struct {
287+ *mock.Call
288+}
289+
290+// DownloadAsset is a helper method to define mock.On call
291+// - ctx context.Context
292+// - p domain.Principal
293+// - repo domain.RepoName
294+// - ref ports.AssetRef
295+// - offset int64
296+// - length int64
297+// - send func(ports.AssetChunk) error
298+func (_e *ReleaseService_Expecter) DownloadAsset(ctx any, p any, repo any, ref any, offset any, length any, send any) *ReleaseService_DownloadAsset_Call {
299+ return &ReleaseService_DownloadAsset_Call{Call: _e.mock.On("DownloadAsset", ctx, p, repo, ref, offset, length, send)}
300+}
301+
302+func (_c *ReleaseService_DownloadAsset_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.AssetRef, offset int64, length int64, send func(ports.AssetChunk) error)) *ReleaseService_DownloadAsset_Call {
303+ _c.Call.Run(func(args mock.Arguments) {
304+ var arg0 context.Context
305+ if args[0] != nil {
306+ arg0 = args[0].(context.Context)
307+ }
308+ var arg1 domain.Principal
309+ if args[1] != nil {
310+ arg1 = args[1].(domain.Principal)
311+ }
312+ var arg2 domain.RepoName
313+ if args[2] != nil {
314+ arg2 = args[2].(domain.RepoName)
315+ }
316+ var arg3 ports.AssetRef
317+ if args[3] != nil {
318+ arg3 = args[3].(ports.AssetRef)
319+ }
320+ var arg4 int64
321+ if args[4] != nil {
322+ arg4 = args[4].(int64)
323+ }
324+ var arg5 int64
325+ if args[5] != nil {
326+ arg5 = args[5].(int64)
327+ }
328+ var arg6 func(ports.AssetChunk) error
329+ if args[6] != nil {
330+ arg6 = args[6].(func(ports.AssetChunk) error)
331+ }
332+ run(
333+ arg0,
334+ arg1,
335+ arg2,
336+ arg3,
337+ arg4,
338+ arg5,
339+ arg6,
340+ )
341+ })
342+ return _c
343+}
344+
345+func (_c *ReleaseService_DownloadAsset_Call) Return(err error) *ReleaseService_DownloadAsset_Call {
346+ _c.Call.Return(err)
347+ return _c
348+}
349+
350+func (_c *ReleaseService_DownloadAsset_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.AssetRef, offset int64, length int64, send func(ports.AssetChunk) error) error) *ReleaseService_DownloadAsset_Call {
351+ _c.Call.Return(run)
352+ return _c
353+}
354+
355+// GetAsset provides a mock function for the type ReleaseService
356+func (_mock *ReleaseService) GetAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.AssetRef) (domain.ReleaseAsset, error) {
357+ ret := _mock.Called(ctx, p, repo, ref)
358+
359+ if len(ret) == 0 {
360+ panic("no return value specified for GetAsset")
361+ }
362+
363+ var r0 domain.ReleaseAsset
364+ var r1 error
365+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.AssetRef) (domain.ReleaseAsset, error)); ok {
366+ return returnFunc(ctx, p, repo, ref)
367+ }
368+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.AssetRef) domain.ReleaseAsset); ok {
369+ r0 = returnFunc(ctx, p, repo, ref)
370+ } else {
371+ r0 = ret.Get(0).(domain.ReleaseAsset)
372+ }
373+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, ports.AssetRef) error); ok {
374+ r1 = returnFunc(ctx, p, repo, ref)
375+ } else {
376+ r1 = ret.Error(1)
377+ }
378+ return r0, r1
379+}
380+
381+// ReleaseService_GetAsset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAsset'
382+type ReleaseService_GetAsset_Call struct {
383+ *mock.Call
384+}
385+
386+// GetAsset is a helper method to define mock.On call
387+// - ctx context.Context
388+// - p domain.Principal
389+// - repo domain.RepoName
390+// - ref ports.AssetRef
391+func (_e *ReleaseService_Expecter) GetAsset(ctx any, p any, repo any, ref any) *ReleaseService_GetAsset_Call {
392+ return &ReleaseService_GetAsset_Call{Call: _e.mock.On("GetAsset", ctx, p, repo, ref)}
393+}
394+
395+func (_c *ReleaseService_GetAsset_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.AssetRef)) *ReleaseService_GetAsset_Call {
396+ _c.Call.Run(func(args mock.Arguments) {
397+ var arg0 context.Context
398+ if args[0] != nil {
399+ arg0 = args[0].(context.Context)
400+ }
401+ var arg1 domain.Principal
402+ if args[1] != nil {
403+ arg1 = args[1].(domain.Principal)
404+ }
405+ var arg2 domain.RepoName
406+ if args[2] != nil {
407+ arg2 = args[2].(domain.RepoName)
408+ }
409+ var arg3 ports.AssetRef
410+ if args[3] != nil {
411+ arg3 = args[3].(ports.AssetRef)
412+ }
413+ run(
414+ arg0,
415+ arg1,
416+ arg2,
417+ arg3,
418+ )
419+ })
420+ return _c
421+}
422+
423+func (_c *ReleaseService_GetAsset_Call) Return(releaseAsset domain.ReleaseAsset, err error) *ReleaseService_GetAsset_Call {
424+ _c.Call.Return(releaseAsset, err)
425+ return _c
426+}
427+
428+func (_c *ReleaseService_GetAsset_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.AssetRef) (domain.ReleaseAsset, error)) *ReleaseService_GetAsset_Call {
429+ _c.Call.Return(run)
430+ return _c
431+}
432+
433+// GetRelease provides a mock function for the type ReleaseService
434+func (_mock *ReleaseService) GetRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.ReleaseRef) (domain.Release, error) {
435+ ret := _mock.Called(ctx, p, repo, ref)
436+
437+ if len(ret) == 0 {
438+ panic("no return value specified for GetRelease")
439+ }
440+
441+ var r0 domain.Release
442+ var r1 error
443+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.ReleaseRef) (domain.Release, error)); ok {
444+ return returnFunc(ctx, p, repo, ref)
445+ }
446+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.ReleaseRef) domain.Release); ok {
447+ r0 = returnFunc(ctx, p, repo, ref)
448+ } else {
449+ r0 = ret.Get(0).(domain.Release)
450+ }
451+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, ports.ReleaseRef) error); ok {
452+ r1 = returnFunc(ctx, p, repo, ref)
453+ } else {
454+ r1 = ret.Error(1)
455+ }
456+ return r0, r1
457+}
458+
459+// ReleaseService_GetRelease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRelease'
460+type ReleaseService_GetRelease_Call struct {
461+ *mock.Call
462+}
463+
464+// GetRelease is a helper method to define mock.On call
465+// - ctx context.Context
466+// - p domain.Principal
467+// - repo domain.RepoName
468+// - ref ports.ReleaseRef
469+func (_e *ReleaseService_Expecter) GetRelease(ctx any, p any, repo any, ref any) *ReleaseService_GetRelease_Call {
470+ return &ReleaseService_GetRelease_Call{Call: _e.mock.On("GetRelease", ctx, p, repo, ref)}
471+}
472+
473+func (_c *ReleaseService_GetRelease_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.ReleaseRef)) *ReleaseService_GetRelease_Call {
474+ _c.Call.Run(func(args mock.Arguments) {
475+ var arg0 context.Context
476+ if args[0] != nil {
477+ arg0 = args[0].(context.Context)
478+ }
479+ var arg1 domain.Principal
480+ if args[1] != nil {
481+ arg1 = args[1].(domain.Principal)
482+ }
483+ var arg2 domain.RepoName
484+ if args[2] != nil {
485+ arg2 = args[2].(domain.RepoName)
486+ }
487+ var arg3 ports.ReleaseRef
488+ if args[3] != nil {
489+ arg3 = args[3].(ports.ReleaseRef)
490+ }
491+ run(
492+ arg0,
493+ arg1,
494+ arg2,
495+ arg3,
496+ )
497+ })
498+ return _c
499+}
500+
501+func (_c *ReleaseService_GetRelease_Call) Return(release domain.Release, err error) *ReleaseService_GetRelease_Call {
502+ _c.Call.Return(release, err)
503+ return _c
504+}
505+
506+func (_c *ReleaseService_GetRelease_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.ReleaseRef) (domain.Release, error)) *ReleaseService_GetRelease_Call {
507+ _c.Call.Return(run)
508+ return _c
509+}
510+
511+// ListAssets provides a mock function for the type ReleaseService
512+func (_mock *ReleaseService) ListAssets(ctx context.Context, p domain.Principal, repo domain.RepoName, releaseID int64) ([]domain.ReleaseAsset, error) {
513+ ret := _mock.Called(ctx, p, repo, releaseID)
514+
515+ if len(ret) == 0 {
516+ panic("no return value specified for ListAssets")
517+ }
518+
519+ var r0 []domain.ReleaseAsset
520+ var r1 error
521+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) ([]domain.ReleaseAsset, error)); ok {
522+ return returnFunc(ctx, p, repo, releaseID)
523+ }
524+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) []domain.ReleaseAsset); ok {
525+ r0 = returnFunc(ctx, p, repo, releaseID)
526+ } else {
527+ if ret.Get(0) != nil {
528+ r0 = ret.Get(0).([]domain.ReleaseAsset)
529+ }
530+ }
531+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64) error); ok {
532+ r1 = returnFunc(ctx, p, repo, releaseID)
533+ } else {
534+ r1 = ret.Error(1)
535+ }
536+ return r0, r1
537+}
538+
539+// ReleaseService_ListAssets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAssets'
540+type ReleaseService_ListAssets_Call struct {
541+ *mock.Call
542+}
543+
544+// ListAssets is a helper method to define mock.On call
545+// - ctx context.Context
546+// - p domain.Principal
547+// - repo domain.RepoName
548+// - releaseID int64
549+func (_e *ReleaseService_Expecter) ListAssets(ctx any, p any, repo any, releaseID any) *ReleaseService_ListAssets_Call {
550+ return &ReleaseService_ListAssets_Call{Call: _e.mock.On("ListAssets", ctx, p, repo, releaseID)}
551+}
552+
553+func (_c *ReleaseService_ListAssets_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, releaseID int64)) *ReleaseService_ListAssets_Call {
554+ _c.Call.Run(func(args mock.Arguments) {
555+ var arg0 context.Context
556+ if args[0] != nil {
557+ arg0 = args[0].(context.Context)
558+ }
559+ var arg1 domain.Principal
560+ if args[1] != nil {
561+ arg1 = args[1].(domain.Principal)
562+ }
563+ var arg2 domain.RepoName
564+ if args[2] != nil {
565+ arg2 = args[2].(domain.RepoName)
566+ }
567+ var arg3 int64
568+ if args[3] != nil {
569+ arg3 = args[3].(int64)
570+ }
571+ run(
572+ arg0,
573+ arg1,
574+ arg2,
575+ arg3,
576+ )
577+ })
578+ return _c
579+}
580+
581+func (_c *ReleaseService_ListAssets_Call) Return(releaseAssets []domain.ReleaseAsset, err error) *ReleaseService_ListAssets_Call {
582+ _c.Call.Return(releaseAssets, err)
583+ return _c
584+}
585+
586+func (_c *ReleaseService_ListAssets_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, releaseID int64) ([]domain.ReleaseAsset, error)) *ReleaseService_ListAssets_Call {
587+ _c.Call.Return(run)
588+ return _c
589+}
590+
591+// ListReleases provides a mock function for the type ReleaseService
592+func (_mock *ReleaseService) ListReleases(ctx context.Context, p domain.Principal, repo domain.RepoName, pageSize int, pageToken string) (ports.ReleasePage, error) {
593+ ret := _mock.Called(ctx, p, repo, pageSize, pageToken)
594+
595+ if len(ret) == 0 {
596+ panic("no return value specified for ListReleases")
597+ }
598+
599+ var r0 ports.ReleasePage
600+ var r1 error
601+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int, string) (ports.ReleasePage, error)); ok {
602+ return returnFunc(ctx, p, repo, pageSize, pageToken)
603+ }
604+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int, string) ports.ReleasePage); ok {
605+ r0 = returnFunc(ctx, p, repo, pageSize, pageToken)
606+ } else {
607+ r0 = ret.Get(0).(ports.ReleasePage)
608+ }
609+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int, string) error); ok {
610+ r1 = returnFunc(ctx, p, repo, pageSize, pageToken)
611+ } else {
612+ r1 = ret.Error(1)
613+ }
614+ return r0, r1
615+}
616+
617+// ReleaseService_ListReleases_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListReleases'
618+type ReleaseService_ListReleases_Call struct {
619+ *mock.Call
620+}
621+
622+// ListReleases is a helper method to define mock.On call
623+// - ctx context.Context
624+// - p domain.Principal
625+// - repo domain.RepoName
626+// - pageSize int
627+// - pageToken string
628+func (_e *ReleaseService_Expecter) ListReleases(ctx any, p any, repo any, pageSize any, pageToken any) *ReleaseService_ListReleases_Call {
629+ return &ReleaseService_ListReleases_Call{Call: _e.mock.On("ListReleases", ctx, p, repo, pageSize, pageToken)}
630+}
631+
632+func (_c *ReleaseService_ListReleases_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, pageSize int, pageToken string)) *ReleaseService_ListReleases_Call {
633+ _c.Call.Run(func(args mock.Arguments) {
634+ var arg0 context.Context
635+ if args[0] != nil {
636+ arg0 = args[0].(context.Context)
637+ }
638+ var arg1 domain.Principal
639+ if args[1] != nil {
640+ arg1 = args[1].(domain.Principal)
641+ }
642+ var arg2 domain.RepoName
643+ if args[2] != nil {
644+ arg2 = args[2].(domain.RepoName)
645+ }
646+ var arg3 int
647+ if args[3] != nil {
648+ arg3 = args[3].(int)
649+ }
650+ var arg4 string
651+ if args[4] != nil {
652+ arg4 = args[4].(string)
653+ }
654+ run(
655+ arg0,
656+ arg1,
657+ arg2,
658+ arg3,
659+ arg4,
660+ )
661+ })
662+ return _c
663+}
664+
665+func (_c *ReleaseService_ListReleases_Call) Return(releasePage ports.ReleasePage, err error) *ReleaseService_ListReleases_Call {
666+ _c.Call.Return(releasePage, err)
667+ return _c
668+}
669+
670+func (_c *ReleaseService_ListReleases_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, pageSize int, pageToken string) (ports.ReleasePage, error)) *ReleaseService_ListReleases_Call {
671+ _c.Call.Return(run)
672+ return _c
673+}
674+
675+// UpdateAsset provides a mock function for the type ReleaseService
676+func (_mock *ReleaseService) UpdateAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64, params ports.UpdateAssetParams) (domain.ReleaseAsset, error) {
677+ ret := _mock.Called(ctx, p, repo, assetID, params)
678+
679+ if len(ret) == 0 {
680+ panic("no return value specified for UpdateAsset")
681+ }
682+
683+ var r0 domain.ReleaseAsset
684+ var r1 error
685+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.UpdateAssetParams) (domain.ReleaseAsset, error)); ok {
686+ return returnFunc(ctx, p, repo, assetID, params)
687+ }
688+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.UpdateAssetParams) domain.ReleaseAsset); ok {
689+ r0 = returnFunc(ctx, p, repo, assetID, params)
690+ } else {
691+ r0 = ret.Get(0).(domain.ReleaseAsset)
692+ }
693+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.UpdateAssetParams) error); ok {
694+ r1 = returnFunc(ctx, p, repo, assetID, params)
695+ } else {
696+ r1 = ret.Error(1)
697+ }
698+ return r0, r1
699+}
700+
701+// ReleaseService_UpdateAsset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateAsset'
702+type ReleaseService_UpdateAsset_Call struct {
703+ *mock.Call
704+}
705+
706+// UpdateAsset is a helper method to define mock.On call
707+// - ctx context.Context
708+// - p domain.Principal
709+// - repo domain.RepoName
710+// - assetID int64
711+// - params ports.UpdateAssetParams
712+func (_e *ReleaseService_Expecter) UpdateAsset(ctx any, p any, repo any, assetID any, params any) *ReleaseService_UpdateAsset_Call {
713+ return &ReleaseService_UpdateAsset_Call{Call: _e.mock.On("UpdateAsset", ctx, p, repo, assetID, params)}
714+}
715+
716+func (_c *ReleaseService_UpdateAsset_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64, params ports.UpdateAssetParams)) *ReleaseService_UpdateAsset_Call {
717+ _c.Call.Run(func(args mock.Arguments) {
718+ var arg0 context.Context
719+ if args[0] != nil {
720+ arg0 = args[0].(context.Context)
721+ }
722+ var arg1 domain.Principal
723+ if args[1] != nil {
724+ arg1 = args[1].(domain.Principal)
725+ }
726+ var arg2 domain.RepoName
727+ if args[2] != nil {
728+ arg2 = args[2].(domain.RepoName)
729+ }
730+ var arg3 int64
731+ if args[3] != nil {
732+ arg3 = args[3].(int64)
733+ }
734+ var arg4 ports.UpdateAssetParams
735+ if args[4] != nil {
736+ arg4 = args[4].(ports.UpdateAssetParams)
737+ }
738+ run(
739+ arg0,
740+ arg1,
741+ arg2,
742+ arg3,
743+ arg4,
744+ )
745+ })
746+ return _c
747+}
748+
749+func (_c *ReleaseService_UpdateAsset_Call) Return(releaseAsset domain.ReleaseAsset, err error) *ReleaseService_UpdateAsset_Call {
750+ _c.Call.Return(releaseAsset, err)
751+ return _c
752+}
753+
754+func (_c *ReleaseService_UpdateAsset_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64, params ports.UpdateAssetParams) (domain.ReleaseAsset, error)) *ReleaseService_UpdateAsset_Call {
755+ _c.Call.Return(run)
756+ return _c
757+}
758+
759+// UpdateRelease provides a mock function for the type ReleaseService
760+func (_mock *ReleaseService) UpdateRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64, params ports.UpdateReleaseParams) (domain.Release, error) {
761+ ret := _mock.Called(ctx, p, repo, id, params)
762+
763+ if len(ret) == 0 {
764+ panic("no return value specified for UpdateRelease")
765+ }
766+
767+ var r0 domain.Release
768+ var r1 error
769+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.UpdateReleaseParams) (domain.Release, error)); ok {
770+ return returnFunc(ctx, p, repo, id, params)
771+ }
772+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.UpdateReleaseParams) domain.Release); ok {
773+ r0 = returnFunc(ctx, p, repo, id, params)
774+ } else {
775+ r0 = ret.Get(0).(domain.Release)
776+ }
777+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.UpdateReleaseParams) error); ok {
778+ r1 = returnFunc(ctx, p, repo, id, params)
779+ } else {
780+ r1 = ret.Error(1)
781+ }
782+ return r0, r1
783+}
784+
785+// ReleaseService_UpdateRelease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRelease'
786+type ReleaseService_UpdateRelease_Call struct {
787+ *mock.Call
788+}
789+
790+// UpdateRelease is a helper method to define mock.On call
791+// - ctx context.Context
792+// - p domain.Principal
793+// - repo domain.RepoName
794+// - id int64
795+// - params ports.UpdateReleaseParams
796+func (_e *ReleaseService_Expecter) UpdateRelease(ctx any, p any, repo any, id any, params any) *ReleaseService_UpdateRelease_Call {
797+ return &ReleaseService_UpdateRelease_Call{Call: _e.mock.On("UpdateRelease", ctx, p, repo, id, params)}
798+}
799+
800+func (_c *ReleaseService_UpdateRelease_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64, params ports.UpdateReleaseParams)) *ReleaseService_UpdateRelease_Call {
801+ _c.Call.Run(func(args mock.Arguments) {
802+ var arg0 context.Context
803+ if args[0] != nil {
804+ arg0 = args[0].(context.Context)
805+ }
806+ var arg1 domain.Principal
807+ if args[1] != nil {
808+ arg1 = args[1].(domain.Principal)
809+ }
810+ var arg2 domain.RepoName
811+ if args[2] != nil {
812+ arg2 = args[2].(domain.RepoName)
813+ }
814+ var arg3 int64
815+ if args[3] != nil {
816+ arg3 = args[3].(int64)
817+ }
818+ var arg4 ports.UpdateReleaseParams
819+ if args[4] != nil {
820+ arg4 = args[4].(ports.UpdateReleaseParams)
821+ }
822+ run(
823+ arg0,
824+ arg1,
825+ arg2,
826+ arg3,
827+ arg4,
828+ )
829+ })
830+ return _c
831+}
832+
833+func (_c *ReleaseService_UpdateRelease_Call) Return(release domain.Release, err error) *ReleaseService_UpdateRelease_Call {
834+ _c.Call.Return(release, err)
835+ return _c
836+}
837+
838+func (_c *ReleaseService_UpdateRelease_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64, params ports.UpdateReleaseParams) (domain.Release, error)) *ReleaseService_UpdateRelease_Call {
839+ _c.Call.Return(run)
840+ return _c
841+}
842+
843+// UploadAsset provides a mock function for the type ReleaseService
844+func (_mock *ReleaseService) UploadAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.UploadAssetParams, body io.Reader) (domain.ReleaseAsset, error) {
845+ ret := _mock.Called(ctx, p, repo, params, body)
846+
847+ if len(ret) == 0 {
848+ panic("no return value specified for UploadAsset")
849+ }
850+
851+ var r0 domain.ReleaseAsset
852+ var r1 error
853+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.UploadAssetParams, io.Reader) (domain.ReleaseAsset, error)); ok {
854+ return returnFunc(ctx, p, repo, params, body)
855+ }
856+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.UploadAssetParams, io.Reader) domain.ReleaseAsset); ok {
857+ r0 = returnFunc(ctx, p, repo, params, body)
858+ } else {
859+ r0 = ret.Get(0).(domain.ReleaseAsset)
860+ }
861+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, ports.UploadAssetParams, io.Reader) error); ok {
862+ r1 = returnFunc(ctx, p, repo, params, body)
863+ } else {
864+ r1 = ret.Error(1)
865+ }
866+ return r0, r1
867+}
868+
869+// ReleaseService_UploadAsset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UploadAsset'
870+type ReleaseService_UploadAsset_Call struct {
871+ *mock.Call
872+}
873+
874+// UploadAsset is a helper method to define mock.On call
875+// - ctx context.Context
876+// - p domain.Principal
877+// - repo domain.RepoName
878+// - params ports.UploadAssetParams
879+// - body io.Reader
880+func (_e *ReleaseService_Expecter) UploadAsset(ctx any, p any, repo any, params any, body any) *ReleaseService_UploadAsset_Call {
881+ return &ReleaseService_UploadAsset_Call{Call: _e.mock.On("UploadAsset", ctx, p, repo, params, body)}
882+}
883+
884+func (_c *ReleaseService_UploadAsset_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.UploadAssetParams, body io.Reader)) *ReleaseService_UploadAsset_Call {
885+ _c.Call.Run(func(args mock.Arguments) {
886+ var arg0 context.Context
887+ if args[0] != nil {
888+ arg0 = args[0].(context.Context)
889+ }
890+ var arg1 domain.Principal
891+ if args[1] != nil {
892+ arg1 = args[1].(domain.Principal)
893+ }
894+ var arg2 domain.RepoName
895+ if args[2] != nil {
896+ arg2 = args[2].(domain.RepoName)
897+ }
898+ var arg3 ports.UploadAssetParams
899+ if args[3] != nil {
900+ arg3 = args[3].(ports.UploadAssetParams)
901+ }
902+ var arg4 io.Reader
903+ if args[4] != nil {
904+ arg4 = args[4].(io.Reader)
905+ }
906+ run(
907+ arg0,
908+ arg1,
909+ arg2,
910+ arg3,
911+ arg4,
912+ )
913+ })
914+ return _c
915+}
916+
917+func (_c *ReleaseService_UploadAsset_Call) Return(releaseAsset domain.ReleaseAsset, err error) *ReleaseService_UploadAsset_Call {
918+ _c.Call.Return(releaseAsset, err)
919+ return _c
920+}
921+
922+func (_c *ReleaseService_UploadAsset_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.UploadAssetParams, body io.Reader) (domain.ReleaseAsset, error)) *ReleaseService_UploadAsset_Call {
923+ _c.Call.Return(run)
924+ return _c
925+}
926+
927+// Usage provides a mock function for the type ReleaseService
928+func (_mock *ReleaseService) Usage(ctx context.Context, p domain.Principal, repo domain.RepoName) (ports.ReleaseUsage, error) {
929+ ret := _mock.Called(ctx, p, repo)
930+
931+ if len(ret) == 0 {
932+ panic("no return value specified for Usage")
933+ }
934+
935+ var r0 ports.ReleaseUsage
936+ var r1 error
937+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName) (ports.ReleaseUsage, error)); ok {
938+ return returnFunc(ctx, p, repo)
939+ }
940+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName) ports.ReleaseUsage); ok {
941+ r0 = returnFunc(ctx, p, repo)
942+ } else {
943+ r0 = ret.Get(0).(ports.ReleaseUsage)
944+ }
945+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName) error); ok {
946+ r1 = returnFunc(ctx, p, repo)
947+ } else {
948+ r1 = ret.Error(1)
949+ }
950+ return r0, r1
951+}
952+
953+// ReleaseService_Usage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Usage'
954+type ReleaseService_Usage_Call struct {
955+ *mock.Call
956+}
957+
958+// Usage is a helper method to define mock.On call
959+// - ctx context.Context
960+// - p domain.Principal
961+// - repo domain.RepoName
962+func (_e *ReleaseService_Expecter) Usage(ctx any, p any, repo any) *ReleaseService_Usage_Call {
963+ return &ReleaseService_Usage_Call{Call: _e.mock.On("Usage", ctx, p, repo)}
964+}
965+
966+func (_c *ReleaseService_Usage_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName)) *ReleaseService_Usage_Call {
967+ _c.Call.Run(func(args mock.Arguments) {
968+ var arg0 context.Context
969+ if args[0] != nil {
970+ arg0 = args[0].(context.Context)
971+ }
972+ var arg1 domain.Principal
973+ if args[1] != nil {
974+ arg1 = args[1].(domain.Principal)
975+ }
976+ var arg2 domain.RepoName
977+ if args[2] != nil {
978+ arg2 = args[2].(domain.RepoName)
979+ }
980+ run(
981+ arg0,
982+ arg1,
983+ arg2,
984+ )
985+ })
986+ return _c
987+}
988+
989+func (_c *ReleaseService_Usage_Call) Return(releaseUsage ports.ReleaseUsage, err error) *ReleaseService_Usage_Call {
990+ _c.Call.Return(releaseUsage, err)
991+ return _c
992+}
993+
994+func (_c *ReleaseService_Usage_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName) (ports.ReleaseUsage, error)) *ReleaseService_Usage_Call {
995+ _c.Call.Return(run)
996+ return _c
997+}
@@ -0,0 +1,997 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+ "io"
10+
11+ "bnema.dev/ermite/internal/domain"
12+ "bnema.dev/ermite/internal/ports"
13+ mock "github.com/stretchr/testify/mock"
14+)
15+
16+// NewReleaseService creates a new instance of ReleaseService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
17+// The first argument is typically a *testing.T value.
18+func NewReleaseService(t interface {
19+ mock.TestingT
20+ Cleanup(func())
21+}) *ReleaseService {
22+ if helper, ok := t.(interface{ Helper() }); ok {
23+ helper.Helper()
24+ }
25+
26+ mock := &ReleaseService{}
27+ mock.Mock.Test(t)
28+
29+ t.Cleanup(func() {
30+ if helper, ok := t.(interface{ Helper() }); ok {
31+ helper.Helper()
32+ }
33+ mock.AssertExpectations(t)
34+ })
35+
36+ return mock
37+}
38+
39+// ReleaseService is an autogenerated mock type for the ReleaseService type
40+type ReleaseService struct {
41+ mock.Mock
42+}
43+
44+type ReleaseService_Expecter struct {
45+ mock *mock.Mock
46+}
47+
48+func (_m *ReleaseService) EXPECT() *ReleaseService_Expecter {
49+ return &ReleaseService_Expecter{mock: &_m.Mock}
50+}
51+
52+// CreateRelease provides a mock function for the type ReleaseService
53+func (_mock *ReleaseService) CreateRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.CreateReleaseParams) (domain.Release, error) {
54+ ret := _mock.Called(ctx, p, repo, params)
55+
56+ if len(ret) == 0 {
57+ panic("no return value specified for CreateRelease")
58+ }
59+
60+ var r0 domain.Release
61+ var r1 error
62+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.CreateReleaseParams) (domain.Release, error)); ok {
63+ return returnFunc(ctx, p, repo, params)
64+ }
65+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.CreateReleaseParams) domain.Release); ok {
66+ r0 = returnFunc(ctx, p, repo, params)
67+ } else {
68+ r0 = ret.Get(0).(domain.Release)
69+ }
70+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, ports.CreateReleaseParams) error); ok {
71+ r1 = returnFunc(ctx, p, repo, params)
72+ } else {
73+ r1 = ret.Error(1)
74+ }
75+ return r0, r1
76+}
77+
78+// ReleaseService_CreateRelease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateRelease'
79+type ReleaseService_CreateRelease_Call struct {
80+ *mock.Call
81+}
82+
83+// CreateRelease is a helper method to define mock.On call
84+// - ctx context.Context
85+// - p domain.Principal
86+// - repo domain.RepoName
87+// - params ports.CreateReleaseParams
88+func (_e *ReleaseService_Expecter) CreateRelease(ctx any, p any, repo any, params any) *ReleaseService_CreateRelease_Call {
89+ return &ReleaseService_CreateRelease_Call{Call: _e.mock.On("CreateRelease", ctx, p, repo, params)}
90+}
91+
92+func (_c *ReleaseService_CreateRelease_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.CreateReleaseParams)) *ReleaseService_CreateRelease_Call {
93+ _c.Call.Run(func(args mock.Arguments) {
94+ var arg0 context.Context
95+ if args[0] != nil {
96+ arg0 = args[0].(context.Context)
97+ }
98+ var arg1 domain.Principal
99+ if args[1] != nil {
100+ arg1 = args[1].(domain.Principal)
101+ }
102+ var arg2 domain.RepoName
103+ if args[2] != nil {
104+ arg2 = args[2].(domain.RepoName)
105+ }
106+ var arg3 ports.CreateReleaseParams
107+ if args[3] != nil {
108+ arg3 = args[3].(ports.CreateReleaseParams)
109+ }
110+ run(
111+ arg0,
112+ arg1,
113+ arg2,
114+ arg3,
115+ )
116+ })
117+ return _c
118+}
119+
120+func (_c *ReleaseService_CreateRelease_Call) Return(release domain.Release, err error) *ReleaseService_CreateRelease_Call {
121+ _c.Call.Return(release, err)
122+ return _c
123+}
124+
125+func (_c *ReleaseService_CreateRelease_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.CreateReleaseParams) (domain.Release, error)) *ReleaseService_CreateRelease_Call {
126+ _c.Call.Return(run)
127+ return _c
128+}
129+
130+// DeleteAsset provides a mock function for the type ReleaseService
131+func (_mock *ReleaseService) DeleteAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64) error {
132+ ret := _mock.Called(ctx, p, repo, assetID)
133+
134+ if len(ret) == 0 {
135+ panic("no return value specified for DeleteAsset")
136+ }
137+
138+ var r0 error
139+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) error); ok {
140+ r0 = returnFunc(ctx, p, repo, assetID)
141+ } else {
142+ r0 = ret.Error(0)
143+ }
144+ return r0
145+}
146+
147+// ReleaseService_DeleteAsset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAsset'
148+type ReleaseService_DeleteAsset_Call struct {
149+ *mock.Call
150+}
151+
152+// DeleteAsset is a helper method to define mock.On call
153+// - ctx context.Context
154+// - p domain.Principal
155+// - repo domain.RepoName
156+// - assetID int64
157+func (_e *ReleaseService_Expecter) DeleteAsset(ctx any, p any, repo any, assetID any) *ReleaseService_DeleteAsset_Call {
158+ return &ReleaseService_DeleteAsset_Call{Call: _e.mock.On("DeleteAsset", ctx, p, repo, assetID)}
159+}
160+
161+func (_c *ReleaseService_DeleteAsset_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64)) *ReleaseService_DeleteAsset_Call {
162+ _c.Call.Run(func(args mock.Arguments) {
163+ var arg0 context.Context
164+ if args[0] != nil {
165+ arg0 = args[0].(context.Context)
166+ }
167+ var arg1 domain.Principal
168+ if args[1] != nil {
169+ arg1 = args[1].(domain.Principal)
170+ }
171+ var arg2 domain.RepoName
172+ if args[2] != nil {
173+ arg2 = args[2].(domain.RepoName)
174+ }
175+ var arg3 int64
176+ if args[3] != nil {
177+ arg3 = args[3].(int64)
178+ }
179+ run(
180+ arg0,
181+ arg1,
182+ arg2,
183+ arg3,
184+ )
185+ })
186+ return _c
187+}
188+
189+func (_c *ReleaseService_DeleteAsset_Call) Return(err error) *ReleaseService_DeleteAsset_Call {
190+ _c.Call.Return(err)
191+ return _c
192+}
193+
194+func (_c *ReleaseService_DeleteAsset_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64) error) *ReleaseService_DeleteAsset_Call {
195+ _c.Call.Return(run)
196+ return _c
197+}
198+
199+// DeleteRelease provides a mock function for the type ReleaseService
200+func (_mock *ReleaseService) DeleteRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64) error {
201+ ret := _mock.Called(ctx, p, repo, id)
202+
203+ if len(ret) == 0 {
204+ panic("no return value specified for DeleteRelease")
205+ }
206+
207+ var r0 error
208+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) error); ok {
209+ r0 = returnFunc(ctx, p, repo, id)
210+ } else {
211+ r0 = ret.Error(0)
212+ }
213+ return r0
214+}
215+
216+// ReleaseService_DeleteRelease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteRelease'
217+type ReleaseService_DeleteRelease_Call struct {
218+ *mock.Call
219+}
220+
221+// DeleteRelease is a helper method to define mock.On call
222+// - ctx context.Context
223+// - p domain.Principal
224+// - repo domain.RepoName
225+// - id int64
226+func (_e *ReleaseService_Expecter) DeleteRelease(ctx any, p any, repo any, id any) *ReleaseService_DeleteRelease_Call {
227+ return &ReleaseService_DeleteRelease_Call{Call: _e.mock.On("DeleteRelease", ctx, p, repo, id)}
228+}
229+
230+func (_c *ReleaseService_DeleteRelease_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64)) *ReleaseService_DeleteRelease_Call {
231+ _c.Call.Run(func(args mock.Arguments) {
232+ var arg0 context.Context
233+ if args[0] != nil {
234+ arg0 = args[0].(context.Context)
235+ }
236+ var arg1 domain.Principal
237+ if args[1] != nil {
238+ arg1 = args[1].(domain.Principal)
239+ }
240+ var arg2 domain.RepoName
241+ if args[2] != nil {
242+ arg2 = args[2].(domain.RepoName)
243+ }
244+ var arg3 int64
245+ if args[3] != nil {
246+ arg3 = args[3].(int64)
247+ }
248+ run(
249+ arg0,
250+ arg1,
251+ arg2,
252+ arg3,
253+ )
254+ })
255+ return _c
256+}
257+
258+func (_c *ReleaseService_DeleteRelease_Call) Return(err error) *ReleaseService_DeleteRelease_Call {
259+ _c.Call.Return(err)
260+ return _c
261+}
262+
263+func (_c *ReleaseService_DeleteRelease_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64) error) *ReleaseService_DeleteRelease_Call {
264+ _c.Call.Return(run)
265+ return _c
266+}
267+
268+// DownloadAsset provides a mock function for the type ReleaseService
269+func (_mock *ReleaseService) DownloadAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.AssetRef, offset int64, length int64, send func(ports.AssetChunk) error) error {
270+ ret := _mock.Called(ctx, p, repo, ref, offset, length, send)
271+
272+ if len(ret) == 0 {
273+ panic("no return value specified for DownloadAsset")
274+ }
275+
276+ var r0 error
277+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.AssetRef, int64, int64, func(ports.AssetChunk) error) error); ok {
278+ r0 = returnFunc(ctx, p, repo, ref, offset, length, send)
279+ } else {
280+ r0 = ret.Error(0)
281+ }
282+ return r0
283+}
284+
285+// ReleaseService_DownloadAsset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DownloadAsset'
286+type ReleaseService_DownloadAsset_Call struct {
287+ *mock.Call
288+}
289+
290+// DownloadAsset is a helper method to define mock.On call
291+// - ctx context.Context
292+// - p domain.Principal
293+// - repo domain.RepoName
294+// - ref ports.AssetRef
295+// - offset int64
296+// - length int64
297+// - send func(ports.AssetChunk) error
298+func (_e *ReleaseService_Expecter) DownloadAsset(ctx any, p any, repo any, ref any, offset any, length any, send any) *ReleaseService_DownloadAsset_Call {
299+ return &ReleaseService_DownloadAsset_Call{Call: _e.mock.On("DownloadAsset", ctx, p, repo, ref, offset, length, send)}
300+}
301+
302+func (_c *ReleaseService_DownloadAsset_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.AssetRef, offset int64, length int64, send func(ports.AssetChunk) error)) *ReleaseService_DownloadAsset_Call {
303+ _c.Call.Run(func(args mock.Arguments) {
304+ var arg0 context.Context
305+ if args[0] != nil {
306+ arg0 = args[0].(context.Context)
307+ }
308+ var arg1 domain.Principal
309+ if args[1] != nil {
310+ arg1 = args[1].(domain.Principal)
311+ }
312+ var arg2 domain.RepoName
313+ if args[2] != nil {
314+ arg2 = args[2].(domain.RepoName)
315+ }
316+ var arg3 ports.AssetRef
317+ if args[3] != nil {
318+ arg3 = args[3].(ports.AssetRef)
319+ }
320+ var arg4 int64
321+ if args[4] != nil {
322+ arg4 = args[4].(int64)
323+ }
324+ var arg5 int64
325+ if args[5] != nil {
326+ arg5 = args[5].(int64)
327+ }
328+ var arg6 func(ports.AssetChunk) error
329+ if args[6] != nil {
330+ arg6 = args[6].(func(ports.AssetChunk) error)
331+ }
332+ run(
333+ arg0,
334+ arg1,
335+ arg2,
336+ arg3,
337+ arg4,
338+ arg5,
339+ arg6,
340+ )
341+ })
342+ return _c
343+}
344+
345+func (_c *ReleaseService_DownloadAsset_Call) Return(err error) *ReleaseService_DownloadAsset_Call {
346+ _c.Call.Return(err)
347+ return _c
348+}
349+
350+func (_c *ReleaseService_DownloadAsset_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.AssetRef, offset int64, length int64, send func(ports.AssetChunk) error) error) *ReleaseService_DownloadAsset_Call {
351+ _c.Call.Return(run)
352+ return _c
353+}
354+
355+// GetAsset provides a mock function for the type ReleaseService
356+func (_mock *ReleaseService) GetAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.AssetRef) (domain.ReleaseAsset, error) {
357+ ret := _mock.Called(ctx, p, repo, ref)
358+
359+ if len(ret) == 0 {
360+ panic("no return value specified for GetAsset")
361+ }
362+
363+ var r0 domain.ReleaseAsset
364+ var r1 error
365+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.AssetRef) (domain.ReleaseAsset, error)); ok {
366+ return returnFunc(ctx, p, repo, ref)
367+ }
368+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.AssetRef) domain.ReleaseAsset); ok {
369+ r0 = returnFunc(ctx, p, repo, ref)
370+ } else {
371+ r0 = ret.Get(0).(domain.ReleaseAsset)
372+ }
373+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, ports.AssetRef) error); ok {
374+ r1 = returnFunc(ctx, p, repo, ref)
375+ } else {
376+ r1 = ret.Error(1)
377+ }
378+ return r0, r1
379+}
380+
381+// ReleaseService_GetAsset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAsset'
382+type ReleaseService_GetAsset_Call struct {
383+ *mock.Call
384+}
385+
386+// GetAsset is a helper method to define mock.On call
387+// - ctx context.Context
388+// - p domain.Principal
389+// - repo domain.RepoName
390+// - ref ports.AssetRef
391+func (_e *ReleaseService_Expecter) GetAsset(ctx any, p any, repo any, ref any) *ReleaseService_GetAsset_Call {
392+ return &ReleaseService_GetAsset_Call{Call: _e.mock.On("GetAsset", ctx, p, repo, ref)}
393+}
394+
395+func (_c *ReleaseService_GetAsset_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.AssetRef)) *ReleaseService_GetAsset_Call {
396+ _c.Call.Run(func(args mock.Arguments) {
397+ var arg0 context.Context
398+ if args[0] != nil {
399+ arg0 = args[0].(context.Context)
400+ }
401+ var arg1 domain.Principal
402+ if args[1] != nil {
403+ arg1 = args[1].(domain.Principal)
404+ }
405+ var arg2 domain.RepoName
406+ if args[2] != nil {
407+ arg2 = args[2].(domain.RepoName)
408+ }
409+ var arg3 ports.AssetRef
410+ if args[3] != nil {
411+ arg3 = args[3].(ports.AssetRef)
412+ }
413+ run(
414+ arg0,
415+ arg1,
416+ arg2,
417+ arg3,
418+ )
419+ })
420+ return _c
421+}
422+
423+func (_c *ReleaseService_GetAsset_Call) Return(releaseAsset domain.ReleaseAsset, err error) *ReleaseService_GetAsset_Call {
424+ _c.Call.Return(releaseAsset, err)
425+ return _c
426+}
427+
428+func (_c *ReleaseService_GetAsset_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.AssetRef) (domain.ReleaseAsset, error)) *ReleaseService_GetAsset_Call {
429+ _c.Call.Return(run)
430+ return _c
431+}
432+
433+// GetRelease provides a mock function for the type ReleaseService
434+func (_mock *ReleaseService) GetRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.ReleaseRef) (domain.Release, error) {
435+ ret := _mock.Called(ctx, p, repo, ref)
436+
437+ if len(ret) == 0 {
438+ panic("no return value specified for GetRelease")
439+ }
440+
441+ var r0 domain.Release
442+ var r1 error
443+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.ReleaseRef) (domain.Release, error)); ok {
444+ return returnFunc(ctx, p, repo, ref)
445+ }
446+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.ReleaseRef) domain.Release); ok {
447+ r0 = returnFunc(ctx, p, repo, ref)
448+ } else {
449+ r0 = ret.Get(0).(domain.Release)
450+ }
451+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, ports.ReleaseRef) error); ok {
452+ r1 = returnFunc(ctx, p, repo, ref)
453+ } else {
454+ r1 = ret.Error(1)
455+ }
456+ return r0, r1
457+}
458+
459+// ReleaseService_GetRelease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRelease'
460+type ReleaseService_GetRelease_Call struct {
461+ *mock.Call
462+}
463+
464+// GetRelease is a helper method to define mock.On call
465+// - ctx context.Context
466+// - p domain.Principal
467+// - repo domain.RepoName
468+// - ref ports.ReleaseRef
469+func (_e *ReleaseService_Expecter) GetRelease(ctx any, p any, repo any, ref any) *ReleaseService_GetRelease_Call {
470+ return &ReleaseService_GetRelease_Call{Call: _e.mock.On("GetRelease", ctx, p, repo, ref)}
471+}
472+
473+func (_c *ReleaseService_GetRelease_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.ReleaseRef)) *ReleaseService_GetRelease_Call {
474+ _c.Call.Run(func(args mock.Arguments) {
475+ var arg0 context.Context
476+ if args[0] != nil {
477+ arg0 = args[0].(context.Context)
478+ }
479+ var arg1 domain.Principal
480+ if args[1] != nil {
481+ arg1 = args[1].(domain.Principal)
482+ }
483+ var arg2 domain.RepoName
484+ if args[2] != nil {
485+ arg2 = args[2].(domain.RepoName)
486+ }
487+ var arg3 ports.ReleaseRef
488+ if args[3] != nil {
489+ arg3 = args[3].(ports.ReleaseRef)
490+ }
491+ run(
492+ arg0,
493+ arg1,
494+ arg2,
495+ arg3,
496+ )
497+ })
498+ return _c
499+}
500+
501+func (_c *ReleaseService_GetRelease_Call) Return(release domain.Release, err error) *ReleaseService_GetRelease_Call {
502+ _c.Call.Return(release, err)
503+ return _c
504+}
505+
506+func (_c *ReleaseService_GetRelease_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ports.ReleaseRef) (domain.Release, error)) *ReleaseService_GetRelease_Call {
507+ _c.Call.Return(run)
508+ return _c
509+}
510+
511+// ListAssets provides a mock function for the type ReleaseService
512+func (_mock *ReleaseService) ListAssets(ctx context.Context, p domain.Principal, repo domain.RepoName, releaseID int64) ([]domain.ReleaseAsset, error) {
513+ ret := _mock.Called(ctx, p, repo, releaseID)
514+
515+ if len(ret) == 0 {
516+ panic("no return value specified for ListAssets")
517+ }
518+
519+ var r0 []domain.ReleaseAsset
520+ var r1 error
521+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) ([]domain.ReleaseAsset, error)); ok {
522+ return returnFunc(ctx, p, repo, releaseID)
523+ }
524+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) []domain.ReleaseAsset); ok {
525+ r0 = returnFunc(ctx, p, repo, releaseID)
526+ } else {
527+ if ret.Get(0) != nil {
528+ r0 = ret.Get(0).([]domain.ReleaseAsset)
529+ }
530+ }
531+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64) error); ok {
532+ r1 = returnFunc(ctx, p, repo, releaseID)
533+ } else {
534+ r1 = ret.Error(1)
535+ }
536+ return r0, r1
537+}
538+
539+// ReleaseService_ListAssets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAssets'
540+type ReleaseService_ListAssets_Call struct {
541+ *mock.Call
542+}
543+
544+// ListAssets is a helper method to define mock.On call
545+// - ctx context.Context
546+// - p domain.Principal
547+// - repo domain.RepoName
548+// - releaseID int64
549+func (_e *ReleaseService_Expecter) ListAssets(ctx any, p any, repo any, releaseID any) *ReleaseService_ListAssets_Call {
550+ return &ReleaseService_ListAssets_Call{Call: _e.mock.On("ListAssets", ctx, p, repo, releaseID)}
551+}
552+
553+func (_c *ReleaseService_ListAssets_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, releaseID int64)) *ReleaseService_ListAssets_Call {
554+ _c.Call.Run(func(args mock.Arguments) {
555+ var arg0 context.Context
556+ if args[0] != nil {
557+ arg0 = args[0].(context.Context)
558+ }
559+ var arg1 domain.Principal
560+ if args[1] != nil {
561+ arg1 = args[1].(domain.Principal)
562+ }
563+ var arg2 domain.RepoName
564+ if args[2] != nil {
565+ arg2 = args[2].(domain.RepoName)
566+ }
567+ var arg3 int64
568+ if args[3] != nil {
569+ arg3 = args[3].(int64)
570+ }
571+ run(
572+ arg0,
573+ arg1,
574+ arg2,
575+ arg3,
576+ )
577+ })
578+ return _c
579+}
580+
581+func (_c *ReleaseService_ListAssets_Call) Return(releaseAssets []domain.ReleaseAsset, err error) *ReleaseService_ListAssets_Call {
582+ _c.Call.Return(releaseAssets, err)
583+ return _c
584+}
585+
586+func (_c *ReleaseService_ListAssets_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, releaseID int64) ([]domain.ReleaseAsset, error)) *ReleaseService_ListAssets_Call {
587+ _c.Call.Return(run)
588+ return _c
589+}
590+
591+// ListReleases provides a mock function for the type ReleaseService
592+func (_mock *ReleaseService) ListReleases(ctx context.Context, p domain.Principal, repo domain.RepoName, pageSize int, pageToken string) (ports.ReleasePage, error) {
593+ ret := _mock.Called(ctx, p, repo, pageSize, pageToken)
594+
595+ if len(ret) == 0 {
596+ panic("no return value specified for ListReleases")
597+ }
598+
599+ var r0 ports.ReleasePage
600+ var r1 error
601+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int, string) (ports.ReleasePage, error)); ok {
602+ return returnFunc(ctx, p, repo, pageSize, pageToken)
603+ }
604+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int, string) ports.ReleasePage); ok {
605+ r0 = returnFunc(ctx, p, repo, pageSize, pageToken)
606+ } else {
607+ r0 = ret.Get(0).(ports.ReleasePage)
608+ }
609+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int, string) error); ok {
610+ r1 = returnFunc(ctx, p, repo, pageSize, pageToken)
611+ } else {
612+ r1 = ret.Error(1)
613+ }
614+ return r0, r1
615+}
616+
617+// ReleaseService_ListReleases_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListReleases'
618+type ReleaseService_ListReleases_Call struct {
619+ *mock.Call
620+}
621+
622+// ListReleases is a helper method to define mock.On call
623+// - ctx context.Context
624+// - p domain.Principal
625+// - repo domain.RepoName
626+// - pageSize int
627+// - pageToken string
628+func (_e *ReleaseService_Expecter) ListReleases(ctx any, p any, repo any, pageSize any, pageToken any) *ReleaseService_ListReleases_Call {
629+ return &ReleaseService_ListReleases_Call{Call: _e.mock.On("ListReleases", ctx, p, repo, pageSize, pageToken)}
630+}
631+
632+func (_c *ReleaseService_ListReleases_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, pageSize int, pageToken string)) *ReleaseService_ListReleases_Call {
633+ _c.Call.Run(func(args mock.Arguments) {
634+ var arg0 context.Context
635+ if args[0] != nil {
636+ arg0 = args[0].(context.Context)
637+ }
638+ var arg1 domain.Principal
639+ if args[1] != nil {
640+ arg1 = args[1].(domain.Principal)
641+ }
642+ var arg2 domain.RepoName
643+ if args[2] != nil {
644+ arg2 = args[2].(domain.RepoName)
645+ }
646+ var arg3 int
647+ if args[3] != nil {
648+ arg3 = args[3].(int)
649+ }
650+ var arg4 string
651+ if args[4] != nil {
652+ arg4 = args[4].(string)
653+ }
654+ run(
655+ arg0,
656+ arg1,
657+ arg2,
658+ arg3,
659+ arg4,
660+ )
661+ })
662+ return _c
663+}
664+
665+func (_c *ReleaseService_ListReleases_Call) Return(releasePage ports.ReleasePage, err error) *ReleaseService_ListReleases_Call {
666+ _c.Call.Return(releasePage, err)
667+ return _c
668+}
669+
670+func (_c *ReleaseService_ListReleases_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, pageSize int, pageToken string) (ports.ReleasePage, error)) *ReleaseService_ListReleases_Call {
671+ _c.Call.Return(run)
672+ return _c
673+}
674+
675+// UpdateAsset provides a mock function for the type ReleaseService
676+func (_mock *ReleaseService) UpdateAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64, params ports.UpdateAssetParams) (domain.ReleaseAsset, error) {
677+ ret := _mock.Called(ctx, p, repo, assetID, params)
678+
679+ if len(ret) == 0 {
680+ panic("no return value specified for UpdateAsset")
681+ }
682+
683+ var r0 domain.ReleaseAsset
684+ var r1 error
685+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.UpdateAssetParams) (domain.ReleaseAsset, error)); ok {
686+ return returnFunc(ctx, p, repo, assetID, params)
687+ }
688+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.UpdateAssetParams) domain.ReleaseAsset); ok {
689+ r0 = returnFunc(ctx, p, repo, assetID, params)
690+ } else {
691+ r0 = ret.Get(0).(domain.ReleaseAsset)
692+ }
693+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.UpdateAssetParams) error); ok {
694+ r1 = returnFunc(ctx, p, repo, assetID, params)
695+ } else {
696+ r1 = ret.Error(1)
697+ }
698+ return r0, r1
699+}
700+
701+// ReleaseService_UpdateAsset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateAsset'
702+type ReleaseService_UpdateAsset_Call struct {
703+ *mock.Call
704+}
705+
706+// UpdateAsset is a helper method to define mock.On call
707+// - ctx context.Context
708+// - p domain.Principal
709+// - repo domain.RepoName
710+// - assetID int64
711+// - params ports.UpdateAssetParams
712+func (_e *ReleaseService_Expecter) UpdateAsset(ctx any, p any, repo any, assetID any, params any) *ReleaseService_UpdateAsset_Call {
713+ return &ReleaseService_UpdateAsset_Call{Call: _e.mock.On("UpdateAsset", ctx, p, repo, assetID, params)}
714+}
715+
716+func (_c *ReleaseService_UpdateAsset_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64, params ports.UpdateAssetParams)) *ReleaseService_UpdateAsset_Call {
717+ _c.Call.Run(func(args mock.Arguments) {
718+ var arg0 context.Context
719+ if args[0] != nil {
720+ arg0 = args[0].(context.Context)
721+ }
722+ var arg1 domain.Principal
723+ if args[1] != nil {
724+ arg1 = args[1].(domain.Principal)
725+ }
726+ var arg2 domain.RepoName
727+ if args[2] != nil {
728+ arg2 = args[2].(domain.RepoName)
729+ }
730+ var arg3 int64
731+ if args[3] != nil {
732+ arg3 = args[3].(int64)
733+ }
734+ var arg4 ports.UpdateAssetParams
735+ if args[4] != nil {
736+ arg4 = args[4].(ports.UpdateAssetParams)
737+ }
738+ run(
739+ arg0,
740+ arg1,
741+ arg2,
742+ arg3,
743+ arg4,
744+ )
745+ })
746+ return _c
747+}
748+
749+func (_c *ReleaseService_UpdateAsset_Call) Return(releaseAsset domain.ReleaseAsset, err error) *ReleaseService_UpdateAsset_Call {
750+ _c.Call.Return(releaseAsset, err)
751+ return _c
752+}
753+
754+func (_c *ReleaseService_UpdateAsset_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64, params ports.UpdateAssetParams) (domain.ReleaseAsset, error)) *ReleaseService_UpdateAsset_Call {
755+ _c.Call.Return(run)
756+ return _c
757+}
758+
759+// UpdateRelease provides a mock function for the type ReleaseService
760+func (_mock *ReleaseService) UpdateRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64, params ports.UpdateReleaseParams) (domain.Release, error) {
761+ ret := _mock.Called(ctx, p, repo, id, params)
762+
763+ if len(ret) == 0 {
764+ panic("no return value specified for UpdateRelease")
765+ }
766+
767+ var r0 domain.Release
768+ var r1 error
769+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.UpdateReleaseParams) (domain.Release, error)); ok {
770+ return returnFunc(ctx, p, repo, id, params)
771+ }
772+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.UpdateReleaseParams) domain.Release); ok {
773+ r0 = returnFunc(ctx, p, repo, id, params)
774+ } else {
775+ r0 = ret.Get(0).(domain.Release)
776+ }
777+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64, ports.UpdateReleaseParams) error); ok {
778+ r1 = returnFunc(ctx, p, repo, id, params)
779+ } else {
780+ r1 = ret.Error(1)
781+ }
782+ return r0, r1
783+}
784+
785+// ReleaseService_UpdateRelease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRelease'
786+type ReleaseService_UpdateRelease_Call struct {
787+ *mock.Call
788+}
789+
790+// UpdateRelease is a helper method to define mock.On call
791+// - ctx context.Context
792+// - p domain.Principal
793+// - repo domain.RepoName
794+// - id int64
795+// - params ports.UpdateReleaseParams
796+func (_e *ReleaseService_Expecter) UpdateRelease(ctx any, p any, repo any, id any, params any) *ReleaseService_UpdateRelease_Call {
797+ return &ReleaseService_UpdateRelease_Call{Call: _e.mock.On("UpdateRelease", ctx, p, repo, id, params)}
798+}
799+
800+func (_c *ReleaseService_UpdateRelease_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64, params ports.UpdateReleaseParams)) *ReleaseService_UpdateRelease_Call {
801+ _c.Call.Run(func(args mock.Arguments) {
802+ var arg0 context.Context
803+ if args[0] != nil {
804+ arg0 = args[0].(context.Context)
805+ }
806+ var arg1 domain.Principal
807+ if args[1] != nil {
808+ arg1 = args[1].(domain.Principal)
809+ }
810+ var arg2 domain.RepoName
811+ if args[2] != nil {
812+ arg2 = args[2].(domain.RepoName)
813+ }
814+ var arg3 int64
815+ if args[3] != nil {
816+ arg3 = args[3].(int64)
817+ }
818+ var arg4 ports.UpdateReleaseParams
819+ if args[4] != nil {
820+ arg4 = args[4].(ports.UpdateReleaseParams)
821+ }
822+ run(
823+ arg0,
824+ arg1,
825+ arg2,
826+ arg3,
827+ arg4,
828+ )
829+ })
830+ return _c
831+}
832+
833+func (_c *ReleaseService_UpdateRelease_Call) Return(release domain.Release, err error) *ReleaseService_UpdateRelease_Call {
834+ _c.Call.Return(release, err)
835+ return _c
836+}
837+
838+func (_c *ReleaseService_UpdateRelease_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64, params ports.UpdateReleaseParams) (domain.Release, error)) *ReleaseService_UpdateRelease_Call {
839+ _c.Call.Return(run)
840+ return _c
841+}
842+
843+// UploadAsset provides a mock function for the type ReleaseService
844+func (_mock *ReleaseService) UploadAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.UploadAssetParams, body io.Reader) (domain.ReleaseAsset, error) {
845+ ret := _mock.Called(ctx, p, repo, params, body)
846+
847+ if len(ret) == 0 {
848+ panic("no return value specified for UploadAsset")
849+ }
850+
851+ var r0 domain.ReleaseAsset
852+ var r1 error
853+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.UploadAssetParams, io.Reader) (domain.ReleaseAsset, error)); ok {
854+ return returnFunc(ctx, p, repo, params, body)
855+ }
856+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.UploadAssetParams, io.Reader) domain.ReleaseAsset); ok {
857+ r0 = returnFunc(ctx, p, repo, params, body)
858+ } else {
859+ r0 = ret.Get(0).(domain.ReleaseAsset)
860+ }
861+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, ports.UploadAssetParams, io.Reader) error); ok {
862+ r1 = returnFunc(ctx, p, repo, params, body)
863+ } else {
864+ r1 = ret.Error(1)
865+ }
866+ return r0, r1
867+}
868+
869+// ReleaseService_UploadAsset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UploadAsset'
870+type ReleaseService_UploadAsset_Call struct {
871+ *mock.Call
872+}
873+
874+// UploadAsset is a helper method to define mock.On call
875+// - ctx context.Context
876+// - p domain.Principal
877+// - repo domain.RepoName
878+// - params ports.UploadAssetParams
879+// - body io.Reader
880+func (_e *ReleaseService_Expecter) UploadAsset(ctx any, p any, repo any, params any, body any) *ReleaseService_UploadAsset_Call {
881+ return &ReleaseService_UploadAsset_Call{Call: _e.mock.On("UploadAsset", ctx, p, repo, params, body)}
882+}
883+
884+func (_c *ReleaseService_UploadAsset_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.UploadAssetParams, body io.Reader)) *ReleaseService_UploadAsset_Call {
885+ _c.Call.Run(func(args mock.Arguments) {
886+ var arg0 context.Context
887+ if args[0] != nil {
888+ arg0 = args[0].(context.Context)
889+ }
890+ var arg1 domain.Principal
891+ if args[1] != nil {
892+ arg1 = args[1].(domain.Principal)
893+ }
894+ var arg2 domain.RepoName
895+ if args[2] != nil {
896+ arg2 = args[2].(domain.RepoName)
897+ }
898+ var arg3 ports.UploadAssetParams
899+ if args[3] != nil {
900+ arg3 = args[3].(ports.UploadAssetParams)
901+ }
902+ var arg4 io.Reader
903+ if args[4] != nil {
904+ arg4 = args[4].(io.Reader)
905+ }
906+ run(
907+ arg0,
908+ arg1,
909+ arg2,
910+ arg3,
911+ arg4,
912+ )
913+ })
914+ return _c
915+}
916+
917+func (_c *ReleaseService_UploadAsset_Call) Return(releaseAsset domain.ReleaseAsset, err error) *ReleaseService_UploadAsset_Call {
918+ _c.Call.Return(releaseAsset, err)
919+ return _c
920+}
921+
922+func (_c *ReleaseService_UploadAsset_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.UploadAssetParams, body io.Reader) (domain.ReleaseAsset, error)) *ReleaseService_UploadAsset_Call {
923+ _c.Call.Return(run)
924+ return _c
925+}
926+
927+// Usage provides a mock function for the type ReleaseService
928+func (_mock *ReleaseService) Usage(ctx context.Context, p domain.Principal, repo domain.RepoName) (ports.ReleaseUsage, error) {
929+ ret := _mock.Called(ctx, p, repo)
930+
931+ if len(ret) == 0 {
932+ panic("no return value specified for Usage")
933+ }
934+
935+ var r0 ports.ReleaseUsage
936+ var r1 error
937+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName) (ports.ReleaseUsage, error)); ok {
938+ return returnFunc(ctx, p, repo)
939+ }
940+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName) ports.ReleaseUsage); ok {
941+ r0 = returnFunc(ctx, p, repo)
942+ } else {
943+ r0 = ret.Get(0).(ports.ReleaseUsage)
944+ }
945+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName) error); ok {
946+ r1 = returnFunc(ctx, p, repo)
947+ } else {
948+ r1 = ret.Error(1)
949+ }
950+ return r0, r1
951+}
952+
953+// ReleaseService_Usage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Usage'
954+type ReleaseService_Usage_Call struct {
955+ *mock.Call
956+}
957+
958+// Usage is a helper method to define mock.On call
959+// - ctx context.Context
960+// - p domain.Principal
961+// - repo domain.RepoName
962+func (_e *ReleaseService_Expecter) Usage(ctx any, p any, repo any) *ReleaseService_Usage_Call {
963+ return &ReleaseService_Usage_Call{Call: _e.mock.On("Usage", ctx, p, repo)}
964+}
965+
966+func (_c *ReleaseService_Usage_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName)) *ReleaseService_Usage_Call {
967+ _c.Call.Run(func(args mock.Arguments) {
968+ var arg0 context.Context
969+ if args[0] != nil {
970+ arg0 = args[0].(context.Context)
971+ }
972+ var arg1 domain.Principal
973+ if args[1] != nil {
974+ arg1 = args[1].(domain.Principal)
975+ }
976+ var arg2 domain.RepoName
977+ if args[2] != nil {
978+ arg2 = args[2].(domain.RepoName)
979+ }
980+ run(
981+ arg0,
982+ arg1,
983+ arg2,
984+ )
985+ })
986+ return _c
987+}
988+
989+func (_c *ReleaseService_Usage_Call) Return(releaseUsage ports.ReleaseUsage, err error) *ReleaseService_Usage_Call {
990+ _c.Call.Return(releaseUsage, err)
991+ return _c
992+}
993+
994+func (_c *ReleaseService_Usage_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName) (ports.ReleaseUsage, error)) *ReleaseService_Usage_Call {
995+ _c.Call.Return(run)
996+ return _c
997+}
A internal/ports/mocks/ReleaseStore.go +796 -0
@@ -0,0 +1,796 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
12+ mock "github.com/stretchr/testify/mock"
13+)
14+
15+// NewReleaseStore creates a new instance of ReleaseStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
16+// The first argument is typically a *testing.T value.
17+func NewReleaseStore(t interface {
18+ mock.TestingT
19+ Cleanup(func())
20+}) *ReleaseStore {
21+ if helper, ok := t.(interface{ Helper() }); ok {
22+ helper.Helper()
23+ }
24+
25+ mock := &ReleaseStore{}
26+ mock.Mock.Test(t)
27+
28+ t.Cleanup(func() {
29+ if helper, ok := t.(interface{ Helper() }); ok {
30+ helper.Helper()
31+ }
32+ mock.AssertExpectations(t)
33+ })
34+
35+ return mock
36+}
37+
38+// ReleaseStore is an autogenerated mock type for the ReleaseStore type
39+type ReleaseStore struct {
40+ mock.Mock
41+}
42+
43+type ReleaseStore_Expecter struct {
44+ mock *mock.Mock
45+}
46+
47+func (_m *ReleaseStore) EXPECT() *ReleaseStore_Expecter {
48+ return &ReleaseStore_Expecter{mock: &_m.Mock}
49+}
50+
51+// AllAssets provides a mock function for the type ReleaseStore
52+func (_mock *ReleaseStore) AllAssets(ctx context.Context) ([]domain.ReleaseAsset, error) {
53+ ret := _mock.Called(ctx)
54+
55+ if len(ret) == 0 {
56+ panic("no return value specified for AllAssets")
57+ }
58+
59+ var r0 []domain.ReleaseAsset
60+ var r1 error
61+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]domain.ReleaseAsset, error)); ok {
62+ return returnFunc(ctx)
63+ }
64+ if returnFunc, ok := ret.Get(0).(func(context.Context) []domain.ReleaseAsset); ok {
65+ r0 = returnFunc(ctx)
66+ } else {
67+ if ret.Get(0) != nil {
68+ r0 = ret.Get(0).([]domain.ReleaseAsset)
69+ }
70+ }
71+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
72+ r1 = returnFunc(ctx)
73+ } else {
74+ r1 = ret.Error(1)
75+ }
76+ return r0, r1
77+}
78+
79+// ReleaseStore_AllAssets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AllAssets'
80+type ReleaseStore_AllAssets_Call struct {
81+ *mock.Call
82+}
83+
84+// AllAssets is a helper method to define mock.On call
85+// - ctx context.Context
86+func (_e *ReleaseStore_Expecter) AllAssets(ctx any) *ReleaseStore_AllAssets_Call {
87+ return &ReleaseStore_AllAssets_Call{Call: _e.mock.On("AllAssets", ctx)}
88+}
89+
90+func (_c *ReleaseStore_AllAssets_Call) Run(run func(ctx context.Context)) *ReleaseStore_AllAssets_Call {
91+ _c.Call.Run(func(args mock.Arguments) {
92+ var arg0 context.Context
93+ if args[0] != nil {
94+ arg0 = args[0].(context.Context)
95+ }
96+ run(
97+ arg0,
98+ )
99+ })
100+ return _c
101+}
102+
103+func (_c *ReleaseStore_AllAssets_Call) Return(releaseAssets []domain.ReleaseAsset, err error) *ReleaseStore_AllAssets_Call {
104+ _c.Call.Return(releaseAssets, err)
105+ return _c
106+}
107+
108+func (_c *ReleaseStore_AllAssets_Call) RunAndReturn(run func(ctx context.Context) ([]domain.ReleaseAsset, error)) *ReleaseStore_AllAssets_Call {
109+ _c.Call.Return(run)
110+ return _c
111+}
112+
113+// ApplyReleases provides a mock function for the type ReleaseStore
114+func (_mock *ReleaseStore) ApplyReleases(ctx context.Context, w ports.ReleaseWrite) error {
115+ ret := _mock.Called(ctx, w)
116+
117+ if len(ret) == 0 {
118+ panic("no return value specified for ApplyReleases")
119+ }
120+
121+ var r0 error
122+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.ReleaseWrite) error); ok {
123+ r0 = returnFunc(ctx, w)
124+ } else {
125+ r0 = ret.Error(0)
126+ }
127+ return r0
128+}
129+
130+// ReleaseStore_ApplyReleases_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ApplyReleases'
131+type ReleaseStore_ApplyReleases_Call struct {
132+ *mock.Call
133+}
134+
135+// ApplyReleases is a helper method to define mock.On call
136+// - ctx context.Context
137+// - w ports.ReleaseWrite
138+func (_e *ReleaseStore_Expecter) ApplyReleases(ctx any, w any) *ReleaseStore_ApplyReleases_Call {
139+ return &ReleaseStore_ApplyReleases_Call{Call: _e.mock.On("ApplyReleases", ctx, w)}
140+}
141+
142+func (_c *ReleaseStore_ApplyReleases_Call) Run(run func(ctx context.Context, w ports.ReleaseWrite)) *ReleaseStore_ApplyReleases_Call {
143+ _c.Call.Run(func(args mock.Arguments) {
144+ var arg0 context.Context
145+ if args[0] != nil {
146+ arg0 = args[0].(context.Context)
147+ }
148+ var arg1 ports.ReleaseWrite
149+ if args[1] != nil {
150+ arg1 = args[1].(ports.ReleaseWrite)
151+ }
152+ run(
153+ arg0,
154+ arg1,
155+ )
156+ })
157+ return _c
158+}
159+
160+func (_c *ReleaseStore_ApplyReleases_Call) Return(err error) *ReleaseStore_ApplyReleases_Call {
161+ _c.Call.Return(err)
162+ return _c
163+}
164+
165+func (_c *ReleaseStore_ApplyReleases_Call) RunAndReturn(run func(ctx context.Context, w ports.ReleaseWrite) error) *ReleaseStore_ApplyReleases_Call {
166+ _c.Call.Return(run)
167+ return _c
168+}
169+
170+// GetAsset provides a mock function for the type ReleaseStore
171+func (_mock *ReleaseStore) GetAsset(ctx context.Context, id int64) (domain.ReleaseAsset, error) {
172+ ret := _mock.Called(ctx, id)
173+
174+ if len(ret) == 0 {
175+ panic("no return value specified for GetAsset")
176+ }
177+
178+ var r0 domain.ReleaseAsset
179+ var r1 error
180+ if returnFunc, ok := ret.Get(0).(func(context.Context, int64) (domain.ReleaseAsset, error)); ok {
181+ return returnFunc(ctx, id)
182+ }
183+ if returnFunc, ok := ret.Get(0).(func(context.Context, int64) domain.ReleaseAsset); ok {
184+ r0 = returnFunc(ctx, id)
185+ } else {
186+ r0 = ret.Get(0).(domain.ReleaseAsset)
187+ }
188+ if returnFunc, ok := ret.Get(1).(func(context.Context, int64) error); ok {
189+ r1 = returnFunc(ctx, id)
190+ } else {
191+ r1 = ret.Error(1)
192+ }
193+ return r0, r1
194+}
195+
196+// ReleaseStore_GetAsset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAsset'
197+type ReleaseStore_GetAsset_Call struct {
198+ *mock.Call
199+}
200+
201+// GetAsset is a helper method to define mock.On call
202+// - ctx context.Context
203+// - id int64
204+func (_e *ReleaseStore_Expecter) GetAsset(ctx any, id any) *ReleaseStore_GetAsset_Call {
205+ return &ReleaseStore_GetAsset_Call{Call: _e.mock.On("GetAsset", ctx, id)}
206+}
207+
208+func (_c *ReleaseStore_GetAsset_Call) Run(run func(ctx context.Context, id int64)) *ReleaseStore_GetAsset_Call {
209+ _c.Call.Run(func(args mock.Arguments) {
210+ var arg0 context.Context
211+ if args[0] != nil {
212+ arg0 = args[0].(context.Context)
213+ }
214+ var arg1 int64
215+ if args[1] != nil {
216+ arg1 = args[1].(int64)
217+ }
218+ run(
219+ arg0,
220+ arg1,
221+ )
222+ })
223+ return _c
224+}
225+
226+func (_c *ReleaseStore_GetAsset_Call) Return(releaseAsset domain.ReleaseAsset, err error) *ReleaseStore_GetAsset_Call {
227+ _c.Call.Return(releaseAsset, err)
228+ return _c
229+}
230+
231+func (_c *ReleaseStore_GetAsset_Call) RunAndReturn(run func(ctx context.Context, id int64) (domain.ReleaseAsset, error)) *ReleaseStore_GetAsset_Call {
232+ _c.Call.Return(run)
233+ return _c
234+}
235+
236+// GetAssetByName provides a mock function for the type ReleaseStore
237+func (_mock *ReleaseStore) GetAssetByName(ctx context.Context, repo domain.RepoName, releaseID int64, name string) (domain.ReleaseAsset, error) {
238+ ret := _mock.Called(ctx, repo, releaseID, name)
239+
240+ if len(ret) == 0 {
241+ panic("no return value specified for GetAssetByName")
242+ }
243+
244+ var r0 domain.ReleaseAsset
245+ var r1 error
246+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, string) (domain.ReleaseAsset, error)); ok {
247+ return returnFunc(ctx, repo, releaseID, name)
248+ }
249+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, string) domain.ReleaseAsset); ok {
250+ r0 = returnFunc(ctx, repo, releaseID, name)
251+ } else {
252+ r0 = ret.Get(0).(domain.ReleaseAsset)
253+ }
254+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64, string) error); ok {
255+ r1 = returnFunc(ctx, repo, releaseID, name)
256+ } else {
257+ r1 = ret.Error(1)
258+ }
259+ return r0, r1
260+}
261+
262+// ReleaseStore_GetAssetByName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAssetByName'
263+type ReleaseStore_GetAssetByName_Call struct {
264+ *mock.Call
265+}
266+
267+// GetAssetByName is a helper method to define mock.On call
268+// - ctx context.Context
269+// - repo domain.RepoName
270+// - releaseID int64
271+// - name string
272+func (_e *ReleaseStore_Expecter) GetAssetByName(ctx any, repo any, releaseID any, name any) *ReleaseStore_GetAssetByName_Call {
273+ return &ReleaseStore_GetAssetByName_Call{Call: _e.mock.On("GetAssetByName", ctx, repo, releaseID, name)}
274+}
275+
276+func (_c *ReleaseStore_GetAssetByName_Call) Run(run func(ctx context.Context, repo domain.RepoName, releaseID int64, name string)) *ReleaseStore_GetAssetByName_Call {
277+ _c.Call.Run(func(args mock.Arguments) {
278+ var arg0 context.Context
279+ if args[0] != nil {
280+ arg0 = args[0].(context.Context)
281+ }
282+ var arg1 domain.RepoName
283+ if args[1] != nil {
284+ arg1 = args[1].(domain.RepoName)
285+ }
286+ var arg2 int64
287+ if args[2] != nil {
288+ arg2 = args[2].(int64)
289+ }
290+ var arg3 string
291+ if args[3] != nil {
292+ arg3 = args[3].(string)
293+ }
294+ run(
295+ arg0,
296+ arg1,
297+ arg2,
298+ arg3,
299+ )
300+ })
301+ return _c
302+}
303+
304+func (_c *ReleaseStore_GetAssetByName_Call) Return(releaseAsset domain.ReleaseAsset, err error) *ReleaseStore_GetAssetByName_Call {
305+ _c.Call.Return(releaseAsset, err)
306+ return _c
307+}
308+
309+func (_c *ReleaseStore_GetAssetByName_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, releaseID int64, name string) (domain.ReleaseAsset, error)) *ReleaseStore_GetAssetByName_Call {
310+ _c.Call.Return(run)
311+ return _c
312+}
313+
314+// GetRelease provides a mock function for the type ReleaseStore
315+func (_mock *ReleaseStore) GetRelease(ctx context.Context, repo domain.RepoName, id int64) (domain.Release, error) {
316+ ret := _mock.Called(ctx, repo, id)
317+
318+ if len(ret) == 0 {
319+ panic("no return value specified for GetRelease")
320+ }
321+
322+ var r0 domain.Release
323+ var r1 error
324+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) (domain.Release, error)); ok {
325+ return returnFunc(ctx, repo, id)
326+ }
327+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) domain.Release); ok {
328+ r0 = returnFunc(ctx, repo, id)
329+ } else {
330+ r0 = ret.Get(0).(domain.Release)
331+ }
332+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
333+ r1 = returnFunc(ctx, repo, id)
334+ } else {
335+ r1 = ret.Error(1)
336+ }
337+ return r0, r1
338+}
339+
340+// ReleaseStore_GetRelease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRelease'
341+type ReleaseStore_GetRelease_Call struct {
342+ *mock.Call
343+}
344+
345+// GetRelease is a helper method to define mock.On call
346+// - ctx context.Context
347+// - repo domain.RepoName
348+// - id int64
349+func (_e *ReleaseStore_Expecter) GetRelease(ctx any, repo any, id any) *ReleaseStore_GetRelease_Call {
350+ return &ReleaseStore_GetRelease_Call{Call: _e.mock.On("GetRelease", ctx, repo, id)}
351+}
352+
353+func (_c *ReleaseStore_GetRelease_Call) Run(run func(ctx context.Context, repo domain.RepoName, id int64)) *ReleaseStore_GetRelease_Call {
354+ _c.Call.Run(func(args mock.Arguments) {
355+ var arg0 context.Context
356+ if args[0] != nil {
357+ arg0 = args[0].(context.Context)
358+ }
359+ var arg1 domain.RepoName
360+ if args[1] != nil {
361+ arg1 = args[1].(domain.RepoName)
362+ }
363+ var arg2 int64
364+ if args[2] != nil {
365+ arg2 = args[2].(int64)
366+ }
367+ run(
368+ arg0,
369+ arg1,
370+ arg2,
371+ )
372+ })
373+ return _c
374+}
375+
376+func (_c *ReleaseStore_GetRelease_Call) Return(release domain.Release, err error) *ReleaseStore_GetRelease_Call {
377+ _c.Call.Return(release, err)
378+ return _c
379+}
380+
381+func (_c *ReleaseStore_GetRelease_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, id int64) (domain.Release, error)) *ReleaseStore_GetRelease_Call {
382+ _c.Call.Return(run)
383+ return _c
384+}
385+
386+// GetReleaseByTag provides a mock function for the type ReleaseStore
387+func (_mock *ReleaseStore) GetReleaseByTag(ctx context.Context, repo domain.RepoName, tag string) (domain.Release, error) {
388+ ret := _mock.Called(ctx, repo, tag)
389+
390+ if len(ret) == 0 {
391+ panic("no return value specified for GetReleaseByTag")
392+ }
393+
394+ var r0 domain.Release
395+ var r1 error
396+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string) (domain.Release, error)); ok {
397+ return returnFunc(ctx, repo, tag)
398+ }
399+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string) domain.Release); ok {
400+ r0 = returnFunc(ctx, repo, tag)
401+ } else {
402+ r0 = ret.Get(0).(domain.Release)
403+ }
404+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, string) error); ok {
405+ r1 = returnFunc(ctx, repo, tag)
406+ } else {
407+ r1 = ret.Error(1)
408+ }
409+ return r0, r1
410+}
411+
412+// ReleaseStore_GetReleaseByTag_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetReleaseByTag'
413+type ReleaseStore_GetReleaseByTag_Call struct {
414+ *mock.Call
415+}
416+
417+// GetReleaseByTag is a helper method to define mock.On call
418+// - ctx context.Context
419+// - repo domain.RepoName
420+// - tag string
421+func (_e *ReleaseStore_Expecter) GetReleaseByTag(ctx any, repo any, tag any) *ReleaseStore_GetReleaseByTag_Call {
422+ return &ReleaseStore_GetReleaseByTag_Call{Call: _e.mock.On("GetReleaseByTag", ctx, repo, tag)}
423+}
424+
425+func (_c *ReleaseStore_GetReleaseByTag_Call) Run(run func(ctx context.Context, repo domain.RepoName, tag string)) *ReleaseStore_GetReleaseByTag_Call {
426+ _c.Call.Run(func(args mock.Arguments) {
427+ var arg0 context.Context
428+ if args[0] != nil {
429+ arg0 = args[0].(context.Context)
430+ }
431+ var arg1 domain.RepoName
432+ if args[1] != nil {
433+ arg1 = args[1].(domain.RepoName)
434+ }
435+ var arg2 string
436+ if args[2] != nil {
437+ arg2 = args[2].(string)
438+ }
439+ run(
440+ arg0,
441+ arg1,
442+ arg2,
443+ )
444+ })
445+ return _c
446+}
447+
448+func (_c *ReleaseStore_GetReleaseByTag_Call) Return(release domain.Release, err error) *ReleaseStore_GetReleaseByTag_Call {
449+ _c.Call.Return(release, err)
450+ return _c
451+}
452+
453+func (_c *ReleaseStore_GetReleaseByTag_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, tag string) (domain.Release, error)) *ReleaseStore_GetReleaseByTag_Call {
454+ _c.Call.Return(run)
455+ return _c
456+}
457+
458+// ListAssets provides a mock function for the type ReleaseStore
459+func (_mock *ReleaseStore) ListAssets(ctx context.Context, repo domain.RepoName, releaseID int64) ([]domain.ReleaseAsset, error) {
460+ ret := _mock.Called(ctx, repo, releaseID)
461+
462+ if len(ret) == 0 {
463+ panic("no return value specified for ListAssets")
464+ }
465+
466+ var r0 []domain.ReleaseAsset
467+ var r1 error
468+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) ([]domain.ReleaseAsset, error)); ok {
469+ return returnFunc(ctx, repo, releaseID)
470+ }
471+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) []domain.ReleaseAsset); ok {
472+ r0 = returnFunc(ctx, repo, releaseID)
473+ } else {
474+ if ret.Get(0) != nil {
475+ r0 = ret.Get(0).([]domain.ReleaseAsset)
476+ }
477+ }
478+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
479+ r1 = returnFunc(ctx, repo, releaseID)
480+ } else {
481+ r1 = ret.Error(1)
482+ }
483+ return r0, r1
484+}
485+
486+// ReleaseStore_ListAssets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAssets'
487+type ReleaseStore_ListAssets_Call struct {
488+ *mock.Call
489+}
490+
491+// ListAssets is a helper method to define mock.On call
492+// - ctx context.Context
493+// - repo domain.RepoName
494+// - releaseID int64
495+func (_e *ReleaseStore_Expecter) ListAssets(ctx any, repo any, releaseID any) *ReleaseStore_ListAssets_Call {
496+ return &ReleaseStore_ListAssets_Call{Call: _e.mock.On("ListAssets", ctx, repo, releaseID)}
497+}
498+
499+func (_c *ReleaseStore_ListAssets_Call) Run(run func(ctx context.Context, repo domain.RepoName, releaseID int64)) *ReleaseStore_ListAssets_Call {
500+ _c.Call.Run(func(args mock.Arguments) {
501+ var arg0 context.Context
502+ if args[0] != nil {
503+ arg0 = args[0].(context.Context)
504+ }
505+ var arg1 domain.RepoName
506+ if args[1] != nil {
507+ arg1 = args[1].(domain.RepoName)
508+ }
509+ var arg2 int64
510+ if args[2] != nil {
511+ arg2 = args[2].(int64)
512+ }
513+ run(
514+ arg0,
515+ arg1,
516+ arg2,
517+ )
518+ })
519+ return _c
520+}
521+
522+func (_c *ReleaseStore_ListAssets_Call) Return(releaseAssets []domain.ReleaseAsset, err error) *ReleaseStore_ListAssets_Call {
523+ _c.Call.Return(releaseAssets, err)
524+ return _c
525+}
526+
527+func (_c *ReleaseStore_ListAssets_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, releaseID int64) ([]domain.ReleaseAsset, error)) *ReleaseStore_ListAssets_Call {
528+ _c.Call.Return(run)
529+ return _c
530+}
531+
532+// ListReleases provides a mock function for the type ReleaseStore
533+func (_mock *ReleaseStore) ListReleases(ctx context.Context, repo domain.RepoName) ([]domain.Release, error) {
534+ ret := _mock.Called(ctx, repo)
535+
536+ if len(ret) == 0 {
537+ panic("no return value specified for ListReleases")
538+ }
539+
540+ var r0 []domain.Release
541+ var r1 error
542+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) ([]domain.Release, error)); ok {
543+ return returnFunc(ctx, repo)
544+ }
545+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) []domain.Release); ok {
546+ r0 = returnFunc(ctx, repo)
547+ } else {
548+ if ret.Get(0) != nil {
549+ r0 = ret.Get(0).([]domain.Release)
550+ }
551+ }
552+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) error); ok {
553+ r1 = returnFunc(ctx, repo)
554+ } else {
555+ r1 = ret.Error(1)
556+ }
557+ return r0, r1
558+}
559+
560+// ReleaseStore_ListReleases_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListReleases'
561+type ReleaseStore_ListReleases_Call struct {
562+ *mock.Call
563+}
564+
565+// ListReleases is a helper method to define mock.On call
566+// - ctx context.Context
567+// - repo domain.RepoName
568+func (_e *ReleaseStore_Expecter) ListReleases(ctx any, repo any) *ReleaseStore_ListReleases_Call {
569+ return &ReleaseStore_ListReleases_Call{Call: _e.mock.On("ListReleases", ctx, repo)}
570+}
571+
572+func (_c *ReleaseStore_ListReleases_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *ReleaseStore_ListReleases_Call {
573+ _c.Call.Run(func(args mock.Arguments) {
574+ var arg0 context.Context
575+ if args[0] != nil {
576+ arg0 = args[0].(context.Context)
577+ }
578+ var arg1 domain.RepoName
579+ if args[1] != nil {
580+ arg1 = args[1].(domain.RepoName)
581+ }
582+ run(
583+ arg0,
584+ arg1,
585+ )
586+ })
587+ return _c
588+}
589+
590+func (_c *ReleaseStore_ListReleases_Call) Return(releases []domain.Release, err error) *ReleaseStore_ListReleases_Call {
591+ _c.Call.Return(releases, err)
592+ return _c
593+}
594+
595+func (_c *ReleaseStore_ListReleases_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) ([]domain.Release, error)) *ReleaseStore_ListReleases_Call {
596+ _c.Call.Return(run)
597+ return _c
598+}
599+
600+// NextReleaseID provides a mock function for the type ReleaseStore
601+func (_mock *ReleaseStore) NextReleaseID(ctx context.Context) (int64, error) {
602+ ret := _mock.Called(ctx)
603+
604+ if len(ret) == 0 {
605+ panic("no return value specified for NextReleaseID")
606+ }
607+
608+ var r0 int64
609+ var r1 error
610+ if returnFunc, ok := ret.Get(0).(func(context.Context) (int64, error)); ok {
611+ return returnFunc(ctx)
612+ }
613+ if returnFunc, ok := ret.Get(0).(func(context.Context) int64); ok {
614+ r0 = returnFunc(ctx)
615+ } else {
616+ r0 = ret.Get(0).(int64)
617+ }
618+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
619+ r1 = returnFunc(ctx)
620+ } else {
621+ r1 = ret.Error(1)
622+ }
623+ return r0, r1
624+}
625+
626+// ReleaseStore_NextReleaseID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NextReleaseID'
627+type ReleaseStore_NextReleaseID_Call struct {
628+ *mock.Call
629+}
630+
631+// NextReleaseID is a helper method to define mock.On call
632+// - ctx context.Context
633+func (_e *ReleaseStore_Expecter) NextReleaseID(ctx any) *ReleaseStore_NextReleaseID_Call {
634+ return &ReleaseStore_NextReleaseID_Call{Call: _e.mock.On("NextReleaseID", ctx)}
635+}
636+
637+func (_c *ReleaseStore_NextReleaseID_Call) Run(run func(ctx context.Context)) *ReleaseStore_NextReleaseID_Call {
638+ _c.Call.Run(func(args mock.Arguments) {
639+ var arg0 context.Context
640+ if args[0] != nil {
641+ arg0 = args[0].(context.Context)
642+ }
643+ run(
644+ arg0,
645+ )
646+ })
647+ return _c
648+}
649+
650+func (_c *ReleaseStore_NextReleaseID_Call) Return(n int64, err error) *ReleaseStore_NextReleaseID_Call {
651+ _c.Call.Return(n, err)
652+ return _c
653+}
654+
655+func (_c *ReleaseStore_NextReleaseID_Call) RunAndReturn(run func(ctx context.Context) (int64, error)) *ReleaseStore_NextReleaseID_Call {
656+ _c.Call.Return(run)
657+ return _c
658+}
659+
660+// ReleaseRepo provides a mock function for the type ReleaseStore
661+func (_mock *ReleaseStore) ReleaseRepo(ctx context.Context, id int64) (domain.RepoName, error) {
662+ ret := _mock.Called(ctx, id)
663+
664+ if len(ret) == 0 {
665+ panic("no return value specified for ReleaseRepo")
666+ }
667+
668+ var r0 domain.RepoName
669+ var r1 error
670+ if returnFunc, ok := ret.Get(0).(func(context.Context, int64) (domain.RepoName, error)); ok {
671+ return returnFunc(ctx, id)
672+ }
673+ if returnFunc, ok := ret.Get(0).(func(context.Context, int64) domain.RepoName); ok {
674+ r0 = returnFunc(ctx, id)
675+ } else {
676+ r0 = ret.Get(0).(domain.RepoName)
677+ }
678+ if returnFunc, ok := ret.Get(1).(func(context.Context, int64) error); ok {
679+ r1 = returnFunc(ctx, id)
680+ } else {
681+ r1 = ret.Error(1)
682+ }
683+ return r0, r1
684+}
685+
686+// ReleaseStore_ReleaseRepo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReleaseRepo'
687+type ReleaseStore_ReleaseRepo_Call struct {
688+ *mock.Call
689+}
690+
691+// ReleaseRepo is a helper method to define mock.On call
692+// - ctx context.Context
693+// - id int64
694+func (_e *ReleaseStore_Expecter) ReleaseRepo(ctx any, id any) *ReleaseStore_ReleaseRepo_Call {
695+ return &ReleaseStore_ReleaseRepo_Call{Call: _e.mock.On("ReleaseRepo", ctx, id)}
696+}
697+
698+func (_c *ReleaseStore_ReleaseRepo_Call) Run(run func(ctx context.Context, id int64)) *ReleaseStore_ReleaseRepo_Call {
699+ _c.Call.Run(func(args mock.Arguments) {
700+ var arg0 context.Context
701+ if args[0] != nil {
702+ arg0 = args[0].(context.Context)
703+ }
704+ var arg1 int64
705+ if args[1] != nil {
706+ arg1 = args[1].(int64)
707+ }
708+ run(
709+ arg0,
710+ arg1,
711+ )
712+ })
713+ return _c
714+}
715+
716+func (_c *ReleaseStore_ReleaseRepo_Call) Return(repoName domain.RepoName, err error) *ReleaseStore_ReleaseRepo_Call {
717+ _c.Call.Return(repoName, err)
718+ return _c
719+}
720+
721+func (_c *ReleaseStore_ReleaseRepo_Call) RunAndReturn(run func(ctx context.Context, id int64) (domain.RepoName, error)) *ReleaseStore_ReleaseRepo_Call {
722+ _c.Call.Return(run)
723+ return _c
724+}
725+
726+// ReleaseUsage provides a mock function for the type ReleaseStore
727+func (_mock *ReleaseStore) ReleaseUsage(ctx context.Context, repo domain.RepoName) (int64, uint64, error) {
728+ ret := _mock.Called(ctx, repo)
729+
730+ if len(ret) == 0 {
731+ panic("no return value specified for ReleaseUsage")
732+ }
733+
734+ var r0 int64
735+ var r1 uint64
736+ var r2 error
737+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) (int64, uint64, error)); ok {
738+ return returnFunc(ctx, repo)
739+ }
740+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) int64); ok {
741+ r0 = returnFunc(ctx, repo)
742+ } else {
743+ r0 = ret.Get(0).(int64)
744+ }
745+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) uint64); ok {
746+ r1 = returnFunc(ctx, repo)
747+ } else {
748+ r1 = ret.Get(1).(uint64)
749+ }
750+ if returnFunc, ok := ret.Get(2).(func(context.Context, domain.RepoName) error); ok {
751+ r2 = returnFunc(ctx, repo)
752+ } else {
753+ r2 = ret.Error(2)
754+ }
755+ return r0, r1, r2
756+}
757+
758+// ReleaseStore_ReleaseUsage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReleaseUsage'
759+type ReleaseStore_ReleaseUsage_Call struct {
760+ *mock.Call
761+}
762+
763+// ReleaseUsage is a helper method to define mock.On call
764+// - ctx context.Context
765+// - repo domain.RepoName
766+func (_e *ReleaseStore_Expecter) ReleaseUsage(ctx any, repo any) *ReleaseStore_ReleaseUsage_Call {
767+ return &ReleaseStore_ReleaseUsage_Call{Call: _e.mock.On("ReleaseUsage", ctx, repo)}
768+}
769+
770+func (_c *ReleaseStore_ReleaseUsage_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *ReleaseStore_ReleaseUsage_Call {
771+ _c.Call.Run(func(args mock.Arguments) {
772+ var arg0 context.Context
773+ if args[0] != nil {
774+ arg0 = args[0].(context.Context)
775+ }
776+ var arg1 domain.RepoName
777+ if args[1] != nil {
778+ arg1 = args[1].(domain.RepoName)
779+ }
780+ run(
781+ arg0,
782+ arg1,
783+ )
784+ })
785+ return _c
786+}
787+
788+func (_c *ReleaseStore_ReleaseUsage_Call) Return(used int64, version uint64, err error) *ReleaseStore_ReleaseUsage_Call {
789+ _c.Call.Return(used, version, err)
790+ return _c
791+}
792+
793+func (_c *ReleaseStore_ReleaseUsage_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) (int64, uint64, error)) *ReleaseStore_ReleaseUsage_Call {
794+ _c.Call.Return(run)
795+ return _c
796+}
@@ -0,0 +1,796 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
12+ mock "github.com/stretchr/testify/mock"
13+)
14+
15+// NewReleaseStore creates a new instance of ReleaseStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
16+// The first argument is typically a *testing.T value.
17+func NewReleaseStore(t interface {
18+ mock.TestingT
19+ Cleanup(func())
20+}) *ReleaseStore {
21+ if helper, ok := t.(interface{ Helper() }); ok {
22+ helper.Helper()
23+ }
24+
25+ mock := &ReleaseStore{}
26+ mock.Mock.Test(t)
27+
28+ t.Cleanup(func() {
29+ if helper, ok := t.(interface{ Helper() }); ok {
30+ helper.Helper()
31+ }
32+ mock.AssertExpectations(t)
33+ })
34+
35+ return mock
36+}
37+
38+// ReleaseStore is an autogenerated mock type for the ReleaseStore type
39+type ReleaseStore struct {
40+ mock.Mock
41+}
42+
43+type ReleaseStore_Expecter struct {
44+ mock *mock.Mock
45+}
46+
47+func (_m *ReleaseStore) EXPECT() *ReleaseStore_Expecter {
48+ return &ReleaseStore_Expecter{mock: &_m.Mock}
49+}
50+
51+// AllAssets provides a mock function for the type ReleaseStore
52+func (_mock *ReleaseStore) AllAssets(ctx context.Context) ([]domain.ReleaseAsset, error) {
53+ ret := _mock.Called(ctx)
54+
55+ if len(ret) == 0 {
56+ panic("no return value specified for AllAssets")
57+ }
58+
59+ var r0 []domain.ReleaseAsset
60+ var r1 error
61+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]domain.ReleaseAsset, error)); ok {
62+ return returnFunc(ctx)
63+ }
64+ if returnFunc, ok := ret.Get(0).(func(context.Context) []domain.ReleaseAsset); ok {
65+ r0 = returnFunc(ctx)
66+ } else {
67+ if ret.Get(0) != nil {
68+ r0 = ret.Get(0).([]domain.ReleaseAsset)
69+ }
70+ }
71+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
72+ r1 = returnFunc(ctx)
73+ } else {
74+ r1 = ret.Error(1)
75+ }
76+ return r0, r1
77+}
78+
79+// ReleaseStore_AllAssets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AllAssets'
80+type ReleaseStore_AllAssets_Call struct {
81+ *mock.Call
82+}
83+
84+// AllAssets is a helper method to define mock.On call
85+// - ctx context.Context
86+func (_e *ReleaseStore_Expecter) AllAssets(ctx any) *ReleaseStore_AllAssets_Call {
87+ return &ReleaseStore_AllAssets_Call{Call: _e.mock.On("AllAssets", ctx)}
88+}
89+
90+func (_c *ReleaseStore_AllAssets_Call) Run(run func(ctx context.Context)) *ReleaseStore_AllAssets_Call {
91+ _c.Call.Run(func(args mock.Arguments) {
92+ var arg0 context.Context
93+ if args[0] != nil {
94+ arg0 = args[0].(context.Context)
95+ }
96+ run(
97+ arg0,
98+ )
99+ })
100+ return _c
101+}
102+
103+func (_c *ReleaseStore_AllAssets_Call) Return(releaseAssets []domain.ReleaseAsset, err error) *ReleaseStore_AllAssets_Call {
104+ _c.Call.Return(releaseAssets, err)
105+ return _c
106+}
107+
108+func (_c *ReleaseStore_AllAssets_Call) RunAndReturn(run func(ctx context.Context) ([]domain.ReleaseAsset, error)) *ReleaseStore_AllAssets_Call {
109+ _c.Call.Return(run)
110+ return _c
111+}
112+
113+// ApplyReleases provides a mock function for the type ReleaseStore
114+func (_mock *ReleaseStore) ApplyReleases(ctx context.Context, w ports.ReleaseWrite) error {
115+ ret := _mock.Called(ctx, w)
116+
117+ if len(ret) == 0 {
118+ panic("no return value specified for ApplyReleases")
119+ }
120+
121+ var r0 error
122+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.ReleaseWrite) error); ok {
123+ r0 = returnFunc(ctx, w)
124+ } else {
125+ r0 = ret.Error(0)
126+ }
127+ return r0
128+}
129+
130+// ReleaseStore_ApplyReleases_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ApplyReleases'
131+type ReleaseStore_ApplyReleases_Call struct {
132+ *mock.Call
133+}
134+
135+// ApplyReleases is a helper method to define mock.On call
136+// - ctx context.Context
137+// - w ports.ReleaseWrite
138+func (_e *ReleaseStore_Expecter) ApplyReleases(ctx any, w any) *ReleaseStore_ApplyReleases_Call {
139+ return &ReleaseStore_ApplyReleases_Call{Call: _e.mock.On("ApplyReleases", ctx, w)}
140+}
141+
142+func (_c *ReleaseStore_ApplyReleases_Call) Run(run func(ctx context.Context, w ports.ReleaseWrite)) *ReleaseStore_ApplyReleases_Call {
143+ _c.Call.Run(func(args mock.Arguments) {
144+ var arg0 context.Context
145+ if args[0] != nil {
146+ arg0 = args[0].(context.Context)
147+ }
148+ var arg1 ports.ReleaseWrite
149+ if args[1] != nil {
150+ arg1 = args[1].(ports.ReleaseWrite)
151+ }
152+ run(
153+ arg0,
154+ arg1,
155+ )
156+ })
157+ return _c
158+}
159+
160+func (_c *ReleaseStore_ApplyReleases_Call) Return(err error) *ReleaseStore_ApplyReleases_Call {
161+ _c.Call.Return(err)
162+ return _c
163+}
164+
165+func (_c *ReleaseStore_ApplyReleases_Call) RunAndReturn(run func(ctx context.Context, w ports.ReleaseWrite) error) *ReleaseStore_ApplyReleases_Call {
166+ _c.Call.Return(run)
167+ return _c
168+}
169+
170+// GetAsset provides a mock function for the type ReleaseStore
171+func (_mock *ReleaseStore) GetAsset(ctx context.Context, id int64) (domain.ReleaseAsset, error) {
172+ ret := _mock.Called(ctx, id)
173+
174+ if len(ret) == 0 {
175+ panic("no return value specified for GetAsset")
176+ }
177+
178+ var r0 domain.ReleaseAsset
179+ var r1 error
180+ if returnFunc, ok := ret.Get(0).(func(context.Context, int64) (domain.ReleaseAsset, error)); ok {
181+ return returnFunc(ctx, id)
182+ }
183+ if returnFunc, ok := ret.Get(0).(func(context.Context, int64) domain.ReleaseAsset); ok {
184+ r0 = returnFunc(ctx, id)
185+ } else {
186+ r0 = ret.Get(0).(domain.ReleaseAsset)
187+ }
188+ if returnFunc, ok := ret.Get(1).(func(context.Context, int64) error); ok {
189+ r1 = returnFunc(ctx, id)
190+ } else {
191+ r1 = ret.Error(1)
192+ }
193+ return r0, r1
194+}
195+
196+// ReleaseStore_GetAsset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAsset'
197+type ReleaseStore_GetAsset_Call struct {
198+ *mock.Call
199+}
200+
201+// GetAsset is a helper method to define mock.On call
202+// - ctx context.Context
203+// - id int64
204+func (_e *ReleaseStore_Expecter) GetAsset(ctx any, id any) *ReleaseStore_GetAsset_Call {
205+ return &ReleaseStore_GetAsset_Call{Call: _e.mock.On("GetAsset", ctx, id)}
206+}
207+
208+func (_c *ReleaseStore_GetAsset_Call) Run(run func(ctx context.Context, id int64)) *ReleaseStore_GetAsset_Call {
209+ _c.Call.Run(func(args mock.Arguments) {
210+ var arg0 context.Context
211+ if args[0] != nil {
212+ arg0 = args[0].(context.Context)
213+ }
214+ var arg1 int64
215+ if args[1] != nil {
216+ arg1 = args[1].(int64)
217+ }
218+ run(
219+ arg0,
220+ arg1,
221+ )
222+ })
223+ return _c
224+}
225+
226+func (_c *ReleaseStore_GetAsset_Call) Return(releaseAsset domain.ReleaseAsset, err error) *ReleaseStore_GetAsset_Call {
227+ _c.Call.Return(releaseAsset, err)
228+ return _c
229+}
230+
231+func (_c *ReleaseStore_GetAsset_Call) RunAndReturn(run func(ctx context.Context, id int64) (domain.ReleaseAsset, error)) *ReleaseStore_GetAsset_Call {
232+ _c.Call.Return(run)
233+ return _c
234+}
235+
236+// GetAssetByName provides a mock function for the type ReleaseStore
237+func (_mock *ReleaseStore) GetAssetByName(ctx context.Context, repo domain.RepoName, releaseID int64, name string) (domain.ReleaseAsset, error) {
238+ ret := _mock.Called(ctx, repo, releaseID, name)
239+
240+ if len(ret) == 0 {
241+ panic("no return value specified for GetAssetByName")
242+ }
243+
244+ var r0 domain.ReleaseAsset
245+ var r1 error
246+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, string) (domain.ReleaseAsset, error)); ok {
247+ return returnFunc(ctx, repo, releaseID, name)
248+ }
249+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, string) domain.ReleaseAsset); ok {
250+ r0 = returnFunc(ctx, repo, releaseID, name)
251+ } else {
252+ r0 = ret.Get(0).(domain.ReleaseAsset)
253+ }
254+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64, string) error); ok {
255+ r1 = returnFunc(ctx, repo, releaseID, name)
256+ } else {
257+ r1 = ret.Error(1)
258+ }
259+ return r0, r1
260+}
261+
262+// ReleaseStore_GetAssetByName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAssetByName'
263+type ReleaseStore_GetAssetByName_Call struct {
264+ *mock.Call
265+}
266+
267+// GetAssetByName is a helper method to define mock.On call
268+// - ctx context.Context
269+// - repo domain.RepoName
270+// - releaseID int64
271+// - name string
272+func (_e *ReleaseStore_Expecter) GetAssetByName(ctx any, repo any, releaseID any, name any) *ReleaseStore_GetAssetByName_Call {
273+ return &ReleaseStore_GetAssetByName_Call{Call: _e.mock.On("GetAssetByName", ctx, repo, releaseID, name)}
274+}
275+
276+func (_c *ReleaseStore_GetAssetByName_Call) Run(run func(ctx context.Context, repo domain.RepoName, releaseID int64, name string)) *ReleaseStore_GetAssetByName_Call {
277+ _c.Call.Run(func(args mock.Arguments) {
278+ var arg0 context.Context
279+ if args[0] != nil {
280+ arg0 = args[0].(context.Context)
281+ }
282+ var arg1 domain.RepoName
283+ if args[1] != nil {
284+ arg1 = args[1].(domain.RepoName)
285+ }
286+ var arg2 int64
287+ if args[2] != nil {
288+ arg2 = args[2].(int64)
289+ }
290+ var arg3 string
291+ if args[3] != nil {
292+ arg3 = args[3].(string)
293+ }
294+ run(
295+ arg0,
296+ arg1,
297+ arg2,
298+ arg3,
299+ )
300+ })
301+ return _c
302+}
303+
304+func (_c *ReleaseStore_GetAssetByName_Call) Return(releaseAsset domain.ReleaseAsset, err error) *ReleaseStore_GetAssetByName_Call {
305+ _c.Call.Return(releaseAsset, err)
306+ return _c
307+}
308+
309+func (_c *ReleaseStore_GetAssetByName_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, releaseID int64, name string) (domain.ReleaseAsset, error)) *ReleaseStore_GetAssetByName_Call {
310+ _c.Call.Return(run)
311+ return _c
312+}
313+
314+// GetRelease provides a mock function for the type ReleaseStore
315+func (_mock *ReleaseStore) GetRelease(ctx context.Context, repo domain.RepoName, id int64) (domain.Release, error) {
316+ ret := _mock.Called(ctx, repo, id)
317+
318+ if len(ret) == 0 {
319+ panic("no return value specified for GetRelease")
320+ }
321+
322+ var r0 domain.Release
323+ var r1 error
324+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) (domain.Release, error)); ok {
325+ return returnFunc(ctx, repo, id)
326+ }
327+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) domain.Release); ok {
328+ r0 = returnFunc(ctx, repo, id)
329+ } else {
330+ r0 = ret.Get(0).(domain.Release)
331+ }
332+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
333+ r1 = returnFunc(ctx, repo, id)
334+ } else {
335+ r1 = ret.Error(1)
336+ }
337+ return r0, r1
338+}
339+
340+// ReleaseStore_GetRelease_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRelease'
341+type ReleaseStore_GetRelease_Call struct {
342+ *mock.Call
343+}
344+
345+// GetRelease is a helper method to define mock.On call
346+// - ctx context.Context
347+// - repo domain.RepoName
348+// - id int64
349+func (_e *ReleaseStore_Expecter) GetRelease(ctx any, repo any, id any) *ReleaseStore_GetRelease_Call {
350+ return &ReleaseStore_GetRelease_Call{Call: _e.mock.On("GetRelease", ctx, repo, id)}
351+}
352+
353+func (_c *ReleaseStore_GetRelease_Call) Run(run func(ctx context.Context, repo domain.RepoName, id int64)) *ReleaseStore_GetRelease_Call {
354+ _c.Call.Run(func(args mock.Arguments) {
355+ var arg0 context.Context
356+ if args[0] != nil {
357+ arg0 = args[0].(context.Context)
358+ }
359+ var arg1 domain.RepoName
360+ if args[1] != nil {
361+ arg1 = args[1].(domain.RepoName)
362+ }
363+ var arg2 int64
364+ if args[2] != nil {
365+ arg2 = args[2].(int64)
366+ }
367+ run(
368+ arg0,
369+ arg1,
370+ arg2,
371+ )
372+ })
373+ return _c
374+}
375+
376+func (_c *ReleaseStore_GetRelease_Call) Return(release domain.Release, err error) *ReleaseStore_GetRelease_Call {
377+ _c.Call.Return(release, err)
378+ return _c
379+}
380+
381+func (_c *ReleaseStore_GetRelease_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, id int64) (domain.Release, error)) *ReleaseStore_GetRelease_Call {
382+ _c.Call.Return(run)
383+ return _c
384+}
385+
386+// GetReleaseByTag provides a mock function for the type ReleaseStore
387+func (_mock *ReleaseStore) GetReleaseByTag(ctx context.Context, repo domain.RepoName, tag string) (domain.Release, error) {
388+ ret := _mock.Called(ctx, repo, tag)
389+
390+ if len(ret) == 0 {
391+ panic("no return value specified for GetReleaseByTag")
392+ }
393+
394+ var r0 domain.Release
395+ var r1 error
396+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string) (domain.Release, error)); ok {
397+ return returnFunc(ctx, repo, tag)
398+ }
399+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string) domain.Release); ok {
400+ r0 = returnFunc(ctx, repo, tag)
401+ } else {
402+ r0 = ret.Get(0).(domain.Release)
403+ }
404+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, string) error); ok {
405+ r1 = returnFunc(ctx, repo, tag)
406+ } else {
407+ r1 = ret.Error(1)
408+ }
409+ return r0, r1
410+}
411+
412+// ReleaseStore_GetReleaseByTag_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetReleaseByTag'
413+type ReleaseStore_GetReleaseByTag_Call struct {
414+ *mock.Call
415+}
416+
417+// GetReleaseByTag is a helper method to define mock.On call
418+// - ctx context.Context
419+// - repo domain.RepoName
420+// - tag string
421+func (_e *ReleaseStore_Expecter) GetReleaseByTag(ctx any, repo any, tag any) *ReleaseStore_GetReleaseByTag_Call {
422+ return &ReleaseStore_GetReleaseByTag_Call{Call: _e.mock.On("GetReleaseByTag", ctx, repo, tag)}
423+}
424+
425+func (_c *ReleaseStore_GetReleaseByTag_Call) Run(run func(ctx context.Context, repo domain.RepoName, tag string)) *ReleaseStore_GetReleaseByTag_Call {
426+ _c.Call.Run(func(args mock.Arguments) {
427+ var arg0 context.Context
428+ if args[0] != nil {
429+ arg0 = args[0].(context.Context)
430+ }
431+ var arg1 domain.RepoName
432+ if args[1] != nil {
433+ arg1 = args[1].(domain.RepoName)
434+ }
435+ var arg2 string
436+ if args[2] != nil {
437+ arg2 = args[2].(string)
438+ }
439+ run(
440+ arg0,
441+ arg1,
442+ arg2,
443+ )
444+ })
445+ return _c
446+}
447+
448+func (_c *ReleaseStore_GetReleaseByTag_Call) Return(release domain.Release, err error) *ReleaseStore_GetReleaseByTag_Call {
449+ _c.Call.Return(release, err)
450+ return _c
451+}
452+
453+func (_c *ReleaseStore_GetReleaseByTag_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, tag string) (domain.Release, error)) *ReleaseStore_GetReleaseByTag_Call {
454+ _c.Call.Return(run)
455+ return _c
456+}
457+
458+// ListAssets provides a mock function for the type ReleaseStore
459+func (_mock *ReleaseStore) ListAssets(ctx context.Context, repo domain.RepoName, releaseID int64) ([]domain.ReleaseAsset, error) {
460+ ret := _mock.Called(ctx, repo, releaseID)
461+
462+ if len(ret) == 0 {
463+ panic("no return value specified for ListAssets")
464+ }
465+
466+ var r0 []domain.ReleaseAsset
467+ var r1 error
468+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) ([]domain.ReleaseAsset, error)); ok {
469+ return returnFunc(ctx, repo, releaseID)
470+ }
471+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) []domain.ReleaseAsset); ok {
472+ r0 = returnFunc(ctx, repo, releaseID)
473+ } else {
474+ if ret.Get(0) != nil {
475+ r0 = ret.Get(0).([]domain.ReleaseAsset)
476+ }
477+ }
478+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
479+ r1 = returnFunc(ctx, repo, releaseID)
480+ } else {
481+ r1 = ret.Error(1)
482+ }
483+ return r0, r1
484+}
485+
486+// ReleaseStore_ListAssets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAssets'
487+type ReleaseStore_ListAssets_Call struct {
488+ *mock.Call
489+}
490+
491+// ListAssets is a helper method to define mock.On call
492+// - ctx context.Context
493+// - repo domain.RepoName
494+// - releaseID int64
495+func (_e *ReleaseStore_Expecter) ListAssets(ctx any, repo any, releaseID any) *ReleaseStore_ListAssets_Call {
496+ return &ReleaseStore_ListAssets_Call{Call: _e.mock.On("ListAssets", ctx, repo, releaseID)}
497+}
498+
499+func (_c *ReleaseStore_ListAssets_Call) Run(run func(ctx context.Context, repo domain.RepoName, releaseID int64)) *ReleaseStore_ListAssets_Call {
500+ _c.Call.Run(func(args mock.Arguments) {
501+ var arg0 context.Context
502+ if args[0] != nil {
503+ arg0 = args[0].(context.Context)
504+ }
505+ var arg1 domain.RepoName
506+ if args[1] != nil {
507+ arg1 = args[1].(domain.RepoName)
508+ }
509+ var arg2 int64
510+ if args[2] != nil {
511+ arg2 = args[2].(int64)
512+ }
513+ run(
514+ arg0,
515+ arg1,
516+ arg2,
517+ )
518+ })
519+ return _c
520+}
521+
522+func (_c *ReleaseStore_ListAssets_Call) Return(releaseAssets []domain.ReleaseAsset, err error) *ReleaseStore_ListAssets_Call {
523+ _c.Call.Return(releaseAssets, err)
524+ return _c
525+}
526+
527+func (_c *ReleaseStore_ListAssets_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, releaseID int64) ([]domain.ReleaseAsset, error)) *ReleaseStore_ListAssets_Call {
528+ _c.Call.Return(run)
529+ return _c
530+}
531+
532+// ListReleases provides a mock function for the type ReleaseStore
533+func (_mock *ReleaseStore) ListReleases(ctx context.Context, repo domain.RepoName) ([]domain.Release, error) {
534+ ret := _mock.Called(ctx, repo)
535+
536+ if len(ret) == 0 {
537+ panic("no return value specified for ListReleases")
538+ }
539+
540+ var r0 []domain.Release
541+ var r1 error
542+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) ([]domain.Release, error)); ok {
543+ return returnFunc(ctx, repo)
544+ }
545+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) []domain.Release); ok {
546+ r0 = returnFunc(ctx, repo)
547+ } else {
548+ if ret.Get(0) != nil {
549+ r0 = ret.Get(0).([]domain.Release)
550+ }
551+ }
552+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) error); ok {
553+ r1 = returnFunc(ctx, repo)
554+ } else {
555+ r1 = ret.Error(1)
556+ }
557+ return r0, r1
558+}
559+
560+// ReleaseStore_ListReleases_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListReleases'
561+type ReleaseStore_ListReleases_Call struct {
562+ *mock.Call
563+}
564+
565+// ListReleases is a helper method to define mock.On call
566+// - ctx context.Context
567+// - repo domain.RepoName
568+func (_e *ReleaseStore_Expecter) ListReleases(ctx any, repo any) *ReleaseStore_ListReleases_Call {
569+ return &ReleaseStore_ListReleases_Call{Call: _e.mock.On("ListReleases", ctx, repo)}
570+}
571+
572+func (_c *ReleaseStore_ListReleases_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *ReleaseStore_ListReleases_Call {
573+ _c.Call.Run(func(args mock.Arguments) {
574+ var arg0 context.Context
575+ if args[0] != nil {
576+ arg0 = args[0].(context.Context)
577+ }
578+ var arg1 domain.RepoName
579+ if args[1] != nil {
580+ arg1 = args[1].(domain.RepoName)
581+ }
582+ run(
583+ arg0,
584+ arg1,
585+ )
586+ })
587+ return _c
588+}
589+
590+func (_c *ReleaseStore_ListReleases_Call) Return(releases []domain.Release, err error) *ReleaseStore_ListReleases_Call {
591+ _c.Call.Return(releases, err)
592+ return _c
593+}
594+
595+func (_c *ReleaseStore_ListReleases_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) ([]domain.Release, error)) *ReleaseStore_ListReleases_Call {
596+ _c.Call.Return(run)
597+ return _c
598+}
599+
600+// NextReleaseID provides a mock function for the type ReleaseStore
601+func (_mock *ReleaseStore) NextReleaseID(ctx context.Context) (int64, error) {
602+ ret := _mock.Called(ctx)
603+
604+ if len(ret) == 0 {
605+ panic("no return value specified for NextReleaseID")
606+ }
607+
608+ var r0 int64
609+ var r1 error
610+ if returnFunc, ok := ret.Get(0).(func(context.Context) (int64, error)); ok {
611+ return returnFunc(ctx)
612+ }
613+ if returnFunc, ok := ret.Get(0).(func(context.Context) int64); ok {
614+ r0 = returnFunc(ctx)
615+ } else {
616+ r0 = ret.Get(0).(int64)
617+ }
618+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
619+ r1 = returnFunc(ctx)
620+ } else {
621+ r1 = ret.Error(1)
622+ }
623+ return r0, r1
624+}
625+
626+// ReleaseStore_NextReleaseID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NextReleaseID'
627+type ReleaseStore_NextReleaseID_Call struct {
628+ *mock.Call
629+}
630+
631+// NextReleaseID is a helper method to define mock.On call
632+// - ctx context.Context
633+func (_e *ReleaseStore_Expecter) NextReleaseID(ctx any) *ReleaseStore_NextReleaseID_Call {
634+ return &ReleaseStore_NextReleaseID_Call{Call: _e.mock.On("NextReleaseID", ctx)}
635+}
636+
637+func (_c *ReleaseStore_NextReleaseID_Call) Run(run func(ctx context.Context)) *ReleaseStore_NextReleaseID_Call {
638+ _c.Call.Run(func(args mock.Arguments) {
639+ var arg0 context.Context
640+ if args[0] != nil {
641+ arg0 = args[0].(context.Context)
642+ }
643+ run(
644+ arg0,
645+ )
646+ })
647+ return _c
648+}
649+
650+func (_c *ReleaseStore_NextReleaseID_Call) Return(n int64, err error) *ReleaseStore_NextReleaseID_Call {
651+ _c.Call.Return(n, err)
652+ return _c
653+}
654+
655+func (_c *ReleaseStore_NextReleaseID_Call) RunAndReturn(run func(ctx context.Context) (int64, error)) *ReleaseStore_NextReleaseID_Call {
656+ _c.Call.Return(run)
657+ return _c
658+}
659+
660+// ReleaseRepo provides a mock function for the type ReleaseStore
661+func (_mock *ReleaseStore) ReleaseRepo(ctx context.Context, id int64) (domain.RepoName, error) {
662+ ret := _mock.Called(ctx, id)
663+
664+ if len(ret) == 0 {
665+ panic("no return value specified for ReleaseRepo")
666+ }
667+
668+ var r0 domain.RepoName
669+ var r1 error
670+ if returnFunc, ok := ret.Get(0).(func(context.Context, int64) (domain.RepoName, error)); ok {
671+ return returnFunc(ctx, id)
672+ }
673+ if returnFunc, ok := ret.Get(0).(func(context.Context, int64) domain.RepoName); ok {
674+ r0 = returnFunc(ctx, id)
675+ } else {
676+ r0 = ret.Get(0).(domain.RepoName)
677+ }
678+ if returnFunc, ok := ret.Get(1).(func(context.Context, int64) error); ok {
679+ r1 = returnFunc(ctx, id)
680+ } else {
681+ r1 = ret.Error(1)
682+ }
683+ return r0, r1
684+}
685+
686+// ReleaseStore_ReleaseRepo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReleaseRepo'
687+type ReleaseStore_ReleaseRepo_Call struct {
688+ *mock.Call
689+}
690+
691+// ReleaseRepo is a helper method to define mock.On call
692+// - ctx context.Context
693+// - id int64
694+func (_e *ReleaseStore_Expecter) ReleaseRepo(ctx any, id any) *ReleaseStore_ReleaseRepo_Call {
695+ return &ReleaseStore_ReleaseRepo_Call{Call: _e.mock.On("ReleaseRepo", ctx, id)}
696+}
697+
698+func (_c *ReleaseStore_ReleaseRepo_Call) Run(run func(ctx context.Context, id int64)) *ReleaseStore_ReleaseRepo_Call {
699+ _c.Call.Run(func(args mock.Arguments) {
700+ var arg0 context.Context
701+ if args[0] != nil {
702+ arg0 = args[0].(context.Context)
703+ }
704+ var arg1 int64
705+ if args[1] != nil {
706+ arg1 = args[1].(int64)
707+ }
708+ run(
709+ arg0,
710+ arg1,
711+ )
712+ })
713+ return _c
714+}
715+
716+func (_c *ReleaseStore_ReleaseRepo_Call) Return(repoName domain.RepoName, err error) *ReleaseStore_ReleaseRepo_Call {
717+ _c.Call.Return(repoName, err)
718+ return _c
719+}
720+
721+func (_c *ReleaseStore_ReleaseRepo_Call) RunAndReturn(run func(ctx context.Context, id int64) (domain.RepoName, error)) *ReleaseStore_ReleaseRepo_Call {
722+ _c.Call.Return(run)
723+ return _c
724+}
725+
726+// ReleaseUsage provides a mock function for the type ReleaseStore
727+func (_mock *ReleaseStore) ReleaseUsage(ctx context.Context, repo domain.RepoName) (int64, uint64, error) {
728+ ret := _mock.Called(ctx, repo)
729+
730+ if len(ret) == 0 {
731+ panic("no return value specified for ReleaseUsage")
732+ }
733+
734+ var r0 int64
735+ var r1 uint64
736+ var r2 error
737+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) (int64, uint64, error)); ok {
738+ return returnFunc(ctx, repo)
739+ }
740+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) int64); ok {
741+ r0 = returnFunc(ctx, repo)
742+ } else {
743+ r0 = ret.Get(0).(int64)
744+ }
745+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) uint64); ok {
746+ r1 = returnFunc(ctx, repo)
747+ } else {
748+ r1 = ret.Get(1).(uint64)
749+ }
750+ if returnFunc, ok := ret.Get(2).(func(context.Context, domain.RepoName) error); ok {
751+ r2 = returnFunc(ctx, repo)
752+ } else {
753+ r2 = ret.Error(2)
754+ }
755+ return r0, r1, r2
756+}
757+
758+// ReleaseStore_ReleaseUsage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReleaseUsage'
759+type ReleaseStore_ReleaseUsage_Call struct {
760+ *mock.Call
761+}
762+
763+// ReleaseUsage is a helper method to define mock.On call
764+// - ctx context.Context
765+// - repo domain.RepoName
766+func (_e *ReleaseStore_Expecter) ReleaseUsage(ctx any, repo any) *ReleaseStore_ReleaseUsage_Call {
767+ return &ReleaseStore_ReleaseUsage_Call{Call: _e.mock.On("ReleaseUsage", ctx, repo)}
768+}
769+
770+func (_c *ReleaseStore_ReleaseUsage_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *ReleaseStore_ReleaseUsage_Call {
771+ _c.Call.Run(func(args mock.Arguments) {
772+ var arg0 context.Context
773+ if args[0] != nil {
774+ arg0 = args[0].(context.Context)
775+ }
776+ var arg1 domain.RepoName
777+ if args[1] != nil {
778+ arg1 = args[1].(domain.RepoName)
779+ }
780+ run(
781+ arg0,
782+ arg1,
783+ )
784+ })
785+ return _c
786+}
787+
788+func (_c *ReleaseStore_ReleaseUsage_Call) Return(used int64, version uint64, err error) *ReleaseStore_ReleaseUsage_Call {
789+ _c.Call.Return(used, version, err)
790+ return _c
791+}
792+
793+func (_c *ReleaseStore_ReleaseUsage_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) (int64, uint64, error)) *ReleaseStore_ReleaseUsage_Call {
794+ _c.Call.Return(run)
795+ return _c
796+}
A internal/ports/mocks/ReleaseSweeper.go +87 -0
@@ -0,0 +1,87 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// NewReleaseSweeper creates a new instance of ReleaseSweeper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func NewReleaseSweeper(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *ReleaseSweeper {
19+ if helper, ok := t.(interface{ Helper() }); ok {
20+ helper.Helper()
21+ }
22+
23+ mock := &ReleaseSweeper{}
24+ mock.Mock.Test(t)
25+
26+ t.Cleanup(func() {
27+ if helper, ok := t.(interface{ Helper() }); ok {
28+ helper.Helper()
29+ }
30+ mock.AssertExpectations(t)
31+ })
32+
33+ return mock
34+}
35+
36+// ReleaseSweeper is an autogenerated mock type for the ReleaseSweeper type
37+type ReleaseSweeper struct {
38+ mock.Mock
39+}
40+
41+type ReleaseSweeper_Expecter struct {
42+ mock *mock.Mock
43+}
44+
45+func (_m *ReleaseSweeper) EXPECT() *ReleaseSweeper_Expecter {
46+ return &ReleaseSweeper_Expecter{mock: &_m.Mock}
47+}
48+
49+// Sweep provides a mock function for the type ReleaseSweeper
50+func (_mock *ReleaseSweeper) Sweep(ctx context.Context) {
51+ _mock.Called(ctx)
52+ return
53+}
54+
55+// ReleaseSweeper_Sweep_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Sweep'
56+type ReleaseSweeper_Sweep_Call struct {
57+ *mock.Call
58+}
59+
60+// Sweep is a helper method to define mock.On call
61+// - ctx context.Context
62+func (_e *ReleaseSweeper_Expecter) Sweep(ctx any) *ReleaseSweeper_Sweep_Call {
63+ return &ReleaseSweeper_Sweep_Call{Call: _e.mock.On("Sweep", ctx)}
64+}
65+
66+func (_c *ReleaseSweeper_Sweep_Call) Run(run func(ctx context.Context)) *ReleaseSweeper_Sweep_Call {
67+ _c.Call.Run(func(args mock.Arguments) {
68+ var arg0 context.Context
69+ if args[0] != nil {
70+ arg0 = args[0].(context.Context)
71+ }
72+ run(
73+ arg0,
74+ )
75+ })
76+ return _c
77+}
78+
79+func (_c *ReleaseSweeper_Sweep_Call) Return() *ReleaseSweeper_Sweep_Call {
80+ _c.Call.Return()
81+ return _c
82+}
83+
84+func (_c *ReleaseSweeper_Sweep_Call) RunAndReturn(run func(ctx context.Context)) *ReleaseSweeper_Sweep_Call {
85+ _c.Run(run)
86+ return _c
87+}
@@ -0,0 +1,87 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// NewReleaseSweeper creates a new instance of ReleaseSweeper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func NewReleaseSweeper(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *ReleaseSweeper {
19+ if helper, ok := t.(interface{ Helper() }); ok {
20+ helper.Helper()
21+ }
22+
23+ mock := &ReleaseSweeper{}
24+ mock.Mock.Test(t)
25+
26+ t.Cleanup(func() {
27+ if helper, ok := t.(interface{ Helper() }); ok {
28+ helper.Helper()
29+ }
30+ mock.AssertExpectations(t)
31+ })
32+
33+ return mock
34+}
35+
36+// ReleaseSweeper is an autogenerated mock type for the ReleaseSweeper type
37+type ReleaseSweeper struct {
38+ mock.Mock
39+}
40+
41+type ReleaseSweeper_Expecter struct {
42+ mock *mock.Mock
43+}
44+
45+func (_m *ReleaseSweeper) EXPECT() *ReleaseSweeper_Expecter {
46+ return &ReleaseSweeper_Expecter{mock: &_m.Mock}
47+}
48+
49+// Sweep provides a mock function for the type ReleaseSweeper
50+func (_mock *ReleaseSweeper) Sweep(ctx context.Context) {
51+ _mock.Called(ctx)
52+ return
53+}
54+
55+// ReleaseSweeper_Sweep_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Sweep'
56+type ReleaseSweeper_Sweep_Call struct {
57+ *mock.Call
58+}
59+
60+// Sweep is a helper method to define mock.On call
61+// - ctx context.Context
62+func (_e *ReleaseSweeper_Expecter) Sweep(ctx any) *ReleaseSweeper_Sweep_Call {
63+ return &ReleaseSweeper_Sweep_Call{Call: _e.mock.On("Sweep", ctx)}
64+}
65+
66+func (_c *ReleaseSweeper_Sweep_Call) Run(run func(ctx context.Context)) *ReleaseSweeper_Sweep_Call {
67+ _c.Call.Run(func(args mock.Arguments) {
68+ var arg0 context.Context
69+ if args[0] != nil {
70+ arg0 = args[0].(context.Context)
71+ }
72+ run(
73+ arg0,
74+ )
75+ })
76+ return _c
77+}
78+
79+func (_c *ReleaseSweeper_Sweep_Call) Return() *ReleaseSweeper_Sweep_Call {
80+ _c.Call.Return()
81+ return _c
82+}
83+
84+func (_c *ReleaseSweeper_Sweep_Call) RunAndReturn(run func(ctx context.Context)) *ReleaseSweeper_Sweep_Call {
85+ _c.Run(run)
86+ return _c
87+}
A internal/ports/mocks/ReleaseTagTrigger.go +100 -0
@@ -0,0 +1,100 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewReleaseTagTrigger creates a new instance of ReleaseTagTrigger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewReleaseTagTrigger(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *ReleaseTagTrigger {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &ReleaseTagTrigger{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// ReleaseTagTrigger is an autogenerated mock type for the ReleaseTagTrigger type
38+type ReleaseTagTrigger struct {
39+ mock.Mock
40+}
41+
42+type ReleaseTagTrigger_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *ReleaseTagTrigger) EXPECT() *ReleaseTagTrigger_Expecter {
47+ return &ReleaseTagTrigger_Expecter{mock: &_m.Mock}
48+}
49+
50+// OnRefsMoved provides a mock function for the type ReleaseTagTrigger
51+func (_mock *ReleaseTagTrigger) OnRefsMoved(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate) {
52+ _mock.Called(ctx, repo, updates)
53+ return
54+}
55+
56+// ReleaseTagTrigger_OnRefsMoved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnRefsMoved'
57+type ReleaseTagTrigger_OnRefsMoved_Call struct {
58+ *mock.Call
59+}
60+
61+// OnRefsMoved is a helper method to define mock.On call
62+// - ctx context.Context
63+// - repo domain.RepoName
64+// - updates []domain.RefUpdate
65+func (_e *ReleaseTagTrigger_Expecter) OnRefsMoved(ctx any, repo any, updates any) *ReleaseTagTrigger_OnRefsMoved_Call {
66+ return &ReleaseTagTrigger_OnRefsMoved_Call{Call: _e.mock.On("OnRefsMoved", ctx, repo, updates)}
67+}
68+
69+func (_c *ReleaseTagTrigger_OnRefsMoved_Call) Run(run func(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate)) *ReleaseTagTrigger_OnRefsMoved_Call {
70+ _c.Call.Run(func(args mock.Arguments) {
71+ var arg0 context.Context
72+ if args[0] != nil {
73+ arg0 = args[0].(context.Context)
74+ }
75+ var arg1 domain.RepoName
76+ if args[1] != nil {
77+ arg1 = args[1].(domain.RepoName)
78+ }
79+ var arg2 []domain.RefUpdate
80+ if args[2] != nil {
81+ arg2 = args[2].([]domain.RefUpdate)
82+ }
83+ run(
84+ arg0,
85+ arg1,
86+ arg2,
87+ )
88+ })
89+ return _c
90+}
91+
92+func (_c *ReleaseTagTrigger_OnRefsMoved_Call) Return() *ReleaseTagTrigger_OnRefsMoved_Call {
93+ _c.Call.Return()
94+ return _c
95+}
96+
97+func (_c *ReleaseTagTrigger_OnRefsMoved_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate)) *ReleaseTagTrigger_OnRefsMoved_Call {
98+ _c.Run(run)
99+ return _c
100+}
@@ -0,0 +1,100 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewReleaseTagTrigger creates a new instance of ReleaseTagTrigger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewReleaseTagTrigger(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *ReleaseTagTrigger {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &ReleaseTagTrigger{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// ReleaseTagTrigger is an autogenerated mock type for the ReleaseTagTrigger type
38+type ReleaseTagTrigger struct {
39+ mock.Mock
40+}
41+
42+type ReleaseTagTrigger_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *ReleaseTagTrigger) EXPECT() *ReleaseTagTrigger_Expecter {
47+ return &ReleaseTagTrigger_Expecter{mock: &_m.Mock}
48+}
49+
50+// OnRefsMoved provides a mock function for the type ReleaseTagTrigger
51+func (_mock *ReleaseTagTrigger) OnRefsMoved(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate) {
52+ _mock.Called(ctx, repo, updates)
53+ return
54+}
55+
56+// ReleaseTagTrigger_OnRefsMoved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnRefsMoved'
57+type ReleaseTagTrigger_OnRefsMoved_Call struct {
58+ *mock.Call
59+}
60+
61+// OnRefsMoved is a helper method to define mock.On call
62+// - ctx context.Context
63+// - repo domain.RepoName
64+// - updates []domain.RefUpdate
65+func (_e *ReleaseTagTrigger_Expecter) OnRefsMoved(ctx any, repo any, updates any) *ReleaseTagTrigger_OnRefsMoved_Call {
66+ return &ReleaseTagTrigger_OnRefsMoved_Call{Call: _e.mock.On("OnRefsMoved", ctx, repo, updates)}
67+}
68+
69+func (_c *ReleaseTagTrigger_OnRefsMoved_Call) Run(run func(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate)) *ReleaseTagTrigger_OnRefsMoved_Call {
70+ _c.Call.Run(func(args mock.Arguments) {
71+ var arg0 context.Context
72+ if args[0] != nil {
73+ arg0 = args[0].(context.Context)
74+ }
75+ var arg1 domain.RepoName
76+ if args[1] != nil {
77+ arg1 = args[1].(domain.RepoName)
78+ }
79+ var arg2 []domain.RefUpdate
80+ if args[2] != nil {
81+ arg2 = args[2].([]domain.RefUpdate)
82+ }
83+ run(
84+ arg0,
85+ arg1,
86+ arg2,
87+ )
88+ })
89+ return _c
90+}
91+
92+func (_c *ReleaseTagTrigger_OnRefsMoved_Call) Return() *ReleaseTagTrigger_OnRefsMoved_Call {
93+ _c.Call.Return()
94+ return _c
95+}
96+
97+func (_c *ReleaseTagTrigger_OnRefsMoved_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate)) *ReleaseTagTrigger_OnRefsMoved_Call {
98+ _c.Run(run)
99+ return _c
100+}
A internal/ports/release.go +113 -0
@@ -0,0 +1,113 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package ports
4+
5+import (
6+ "bnema.dev/ermite/internal/domain"
7+ "context"
8+ "io"
9+)
10+
11+// ReleaseWrite atomically changes one repository's metadata. Every write
12+// requires UsageVersion (zero requires absence), and each record's Version.
13+// Used includes both uploading and uploaded assets.
14+type ReleaseWrite struct {
15+ Repo domain.RepoName
16+ Used int64
17+ UsageVersion uint64
18+ PutReleases, DeleteReleases []domain.Release
19+ PutAssets, DeleteAssets []domain.ReleaseAsset
20+}
21+
22+// ReleaseStore keeps metadata, never contents. *kvstore.Releases implements it.
23+type ReleaseStore interface {
24+ NextReleaseID(ctx context.Context) (int64, error)
25+ ReleaseUsage(ctx context.Context, repo domain.RepoName) (used int64, version uint64, err error)
26+ GetRelease(ctx context.Context, repo domain.RepoName, id int64) (domain.Release, error)
27+ GetReleaseByTag(ctx context.Context, repo domain.RepoName, tag string) (domain.Release, error)
28+ ReleaseRepo(ctx context.Context, id int64) (domain.RepoName, error)
29+ ListReleases(ctx context.Context, repo domain.RepoName) ([]domain.Release, error)
30+ GetAsset(ctx context.Context, id int64) (domain.ReleaseAsset, error)
31+ GetAssetByName(ctx context.Context, repo domain.RepoName, releaseID int64, name string) (domain.ReleaseAsset, error)
32+ ListAssets(ctx context.Context, repo domain.RepoName, releaseID int64) ([]domain.ReleaseAsset, error)
33+ AllAssets(ctx context.Context) ([]domain.ReleaseAsset, error)
34+ ApplyReleases(ctx context.Context, w ReleaseWrite) error
35+}
36+
37+// ReleaseLimits defaults to domain.DefaultReleaseAssetMax (ERMITE_RELEASE_ASSET_MAX),
38+// domain.DefaultReleaseAssetsMax (ERMITE_RELEASE_ASSETS_MAX), and
39+// domain.DefaultReleaseQuota (ERMITE_RELEASE_QUOTA).
40+type ReleaseLimits struct{ AssetMax, AssetsMax, Quota int64 }
41+type CreateReleaseParams struct {
42+ Tag, TargetCommitish, Title, Notes string
43+ Draft, Prerelease, MakeLatest bool
44+}
45+type UpdateReleaseParams struct {
46+ Tag, TargetCommitish, Title, Notes *string
47+ Draft, Prerelease, MakeLatest *bool
48+}
49+
50+// ReleaseRef selects exactly one of ID, Tag, Latest.
51+type ReleaseRef struct {
52+ ID int64
53+ Tag string
54+ Latest bool
55+}
56+
57+// AssetRef selects ID, (Tag, Name), or (Latest, Name).
58+type AssetRef struct {
59+ ID int64
60+ Tag, Name string
61+ Latest bool
62+}
63+type UploadAssetParams struct {
64+ ReleaseID int64
65+ Name, Label, ContentType string
66+ Size int64
67+ Replace bool
68+}
69+type UpdateAssetParams struct{ Name, Label *string }
70+type ReleaseUsage struct {
71+ Used int64
72+ Limits ReleaseLimits
73+}
74+type AssetChunk struct {
75+ Asset *domain.ReleaseAsset
76+ Data []byte
77+}
78+type ReleasePage struct {
79+ Releases []domain.Release
80+ NextPageToken string
81+}
82+
83+// ReleaseService manages releases and streams assets; *usecase.Releases implements it.
84+type ReleaseService interface {
85+ CreateRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, params CreateReleaseParams) (domain.Release, error)
86+ GetRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ReleaseRef) (domain.Release, error)
87+ ListReleases(ctx context.Context, p domain.Principal, repo domain.RepoName, pageSize int, pageToken string) (ReleasePage, error)
88+ UpdateRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64, params UpdateReleaseParams) (domain.Release, error)
89+ DeleteRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64) error
90+ ListAssets(ctx context.Context, p domain.Principal, repo domain.RepoName, releaseID int64) ([]domain.ReleaseAsset, error)
91+ UploadAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, params UploadAssetParams, body io.Reader) (domain.ReleaseAsset, error)
92+ UpdateAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64, params UpdateAssetParams) (domain.ReleaseAsset, error)
93+ DeleteAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64) error
94+ GetAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, ref AssetRef) (domain.ReleaseAsset, error)
95+ // DownloadAsset sends metadata first, then chunks at most domain.CIArtifactChunk;
96+ // length less than zero reads to the end.
97+ DownloadAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, ref AssetRef, offset, length int64, send func(AssetChunk) error) error
98+ Usage(ctx context.Context, p domain.Principal, repo domain.RepoName) (ReleaseUsage, error)
99+}
100+
101+// ReleaseTagTrigger handles tag deletion; *usecase.Releases implements it.
102+type ReleaseTagTrigger interface {
103+ OnRefsMoved(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate)
104+}
105+
106+// ReleaseSweeper removes stale assets and blobs; *usecase.Releases implements it.
107+type ReleaseSweeper interface{ Sweep(ctx context.Context) }
108+
109+// ReleaseRepoCleaner removes contents after metadata deletion; *usecase.Releases implements it.
110+type ReleaseRepoCleaner interface {
111+ AssetsOf(ctx context.Context, repo domain.RepoName) ([]domain.ReleaseAsset, error)
112+ OnRepoDeleted(ctx context.Context, repo domain.RepoName, assets []domain.ReleaseAsset)
113+}
@@ -0,0 +1,113 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package ports
4+
5+import (
6+ "bnema.dev/ermite/internal/domain"
7+ "context"
8+ "io"
9+)
10+
11+// ReleaseWrite atomically changes one repository's metadata. Every write
12+// requires UsageVersion (zero requires absence), and each record's Version.
13+// Used includes both uploading and uploaded assets.
14+type ReleaseWrite struct {
15+ Repo domain.RepoName
16+ Used int64
17+ UsageVersion uint64
18+ PutReleases, DeleteReleases []domain.Release
19+ PutAssets, DeleteAssets []domain.ReleaseAsset
20+}
21+
22+// ReleaseStore keeps metadata, never contents. *kvstore.Releases implements it.
23+type ReleaseStore interface {
24+ NextReleaseID(ctx context.Context) (int64, error)
25+ ReleaseUsage(ctx context.Context, repo domain.RepoName) (used int64, version uint64, err error)
26+ GetRelease(ctx context.Context, repo domain.RepoName, id int64) (domain.Release, error)
27+ GetReleaseByTag(ctx context.Context, repo domain.RepoName, tag string) (domain.Release, error)
28+ ReleaseRepo(ctx context.Context, id int64) (domain.RepoName, error)
29+ ListReleases(ctx context.Context, repo domain.RepoName) ([]domain.Release, error)
30+ GetAsset(ctx context.Context, id int64) (domain.ReleaseAsset, error)
31+ GetAssetByName(ctx context.Context, repo domain.RepoName, releaseID int64, name string) (domain.ReleaseAsset, error)
32+ ListAssets(ctx context.Context, repo domain.RepoName, releaseID int64) ([]domain.ReleaseAsset, error)
33+ AllAssets(ctx context.Context) ([]domain.ReleaseAsset, error)
34+ ApplyReleases(ctx context.Context, w ReleaseWrite) error
35+}
36+
37+// ReleaseLimits defaults to domain.DefaultReleaseAssetMax (ERMITE_RELEASE_ASSET_MAX),
38+// domain.DefaultReleaseAssetsMax (ERMITE_RELEASE_ASSETS_MAX), and
39+// domain.DefaultReleaseQuota (ERMITE_RELEASE_QUOTA).
40+type ReleaseLimits struct{ AssetMax, AssetsMax, Quota int64 }
41+type CreateReleaseParams struct {
42+ Tag, TargetCommitish, Title, Notes string
43+ Draft, Prerelease, MakeLatest bool
44+}
45+type UpdateReleaseParams struct {
46+ Tag, TargetCommitish, Title, Notes *string
47+ Draft, Prerelease, MakeLatest *bool
48+}
49+
50+// ReleaseRef selects exactly one of ID, Tag, Latest.
51+type ReleaseRef struct {
52+ ID int64
53+ Tag string
54+ Latest bool
55+}
56+
57+// AssetRef selects ID, (Tag, Name), or (Latest, Name).
58+type AssetRef struct {
59+ ID int64
60+ Tag, Name string
61+ Latest bool
62+}
63+type UploadAssetParams struct {
64+ ReleaseID int64
65+ Name, Label, ContentType string
66+ Size int64
67+ Replace bool
68+}
69+type UpdateAssetParams struct{ Name, Label *string }
70+type ReleaseUsage struct {
71+ Used int64
72+ Limits ReleaseLimits
73+}
74+type AssetChunk struct {
75+ Asset *domain.ReleaseAsset
76+ Data []byte
77+}
78+type ReleasePage struct {
79+ Releases []domain.Release
80+ NextPageToken string
81+}
82+
83+// ReleaseService manages releases and streams assets; *usecase.Releases implements it.
84+type ReleaseService interface {
85+ CreateRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, params CreateReleaseParams) (domain.Release, error)
86+ GetRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, ref ReleaseRef) (domain.Release, error)
87+ ListReleases(ctx context.Context, p domain.Principal, repo domain.RepoName, pageSize int, pageToken string) (ReleasePage, error)
88+ UpdateRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64, params UpdateReleaseParams) (domain.Release, error)
89+ DeleteRelease(ctx context.Context, p domain.Principal, repo domain.RepoName, id int64) error
90+ ListAssets(ctx context.Context, p domain.Principal, repo domain.RepoName, releaseID int64) ([]domain.ReleaseAsset, error)
91+ UploadAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, params UploadAssetParams, body io.Reader) (domain.ReleaseAsset, error)
92+ UpdateAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64, params UpdateAssetParams) (domain.ReleaseAsset, error)
93+ DeleteAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, assetID int64) error
94+ GetAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, ref AssetRef) (domain.ReleaseAsset, error)
95+ // DownloadAsset sends metadata first, then chunks at most domain.CIArtifactChunk;
96+ // length less than zero reads to the end.
97+ DownloadAsset(ctx context.Context, p domain.Principal, repo domain.RepoName, ref AssetRef, offset, length int64, send func(AssetChunk) error) error
98+ Usage(ctx context.Context, p domain.Principal, repo domain.RepoName) (ReleaseUsage, error)
99+}
100+
101+// ReleaseTagTrigger handles tag deletion; *usecase.Releases implements it.
102+type ReleaseTagTrigger interface {
103+ OnRefsMoved(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate)
104+}
105+
106+// ReleaseSweeper removes stale assets and blobs; *usecase.Releases implements it.
107+type ReleaseSweeper interface{ Sweep(ctx context.Context) }
108+
109+// ReleaseRepoCleaner removes contents after metadata deletion; *usecase.Releases implements it.
110+type ReleaseRepoCleaner interface {
111+ AssetsOf(ctx context.Context, repo domain.RepoName) ([]domain.ReleaseAsset, error)
112+ OnRepoDeleted(ctx context.Context, repo domain.RepoName, assets []domain.ReleaseAsset)
113+}
A proto/ermite/storage/release.proto +46 -0
@@ -0,0 +1,46 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+syntax = "proto3";
4+package ermite.storage;
5+import "google/protobuf/timestamp.proto";
6+option go_package = "bnema.dev/ermite/gen/ermite/storage;storagepb";
7+
8+// Release stores release metadata, not object contents.
9+message Release {
10+ int64 id = 1;
11+ string repo = 2;
12+ string tag = 3;
13+ string title = 4;
14+ string notes = 5;
15+ bool draft = 6;
16+ bool prerelease = 7;
17+ bool make_latest = 8;
18+ string target_commitish = 9;
19+ string author = 10;
20+ google.protobuf.Timestamp created_at = 11;
21+ google.protobuf.Timestamp published_at = 12;
22+ google.protobuf.Timestamp updated_at = 13;
23+}
24+
25+// ReleaseAsset stores release metadata, not object contents.
26+message ReleaseAsset {
27+ int64 id = 1;
28+ int64 release_id = 2;
29+ string repo = 3;
30+ string name = 4;
31+ string label = 5;
32+ string content_type = 6;
33+ int64 size = 7;
34+ string digest = 8;
35+ string state = 9;
36+ string uploader = 10;
37+ int64 charged = 11;
38+ string multipart_id = 12;
39+ google.protobuf.Timestamp created_at = 13;
40+ google.protobuf.Timestamp updated_at = 14;
41+}
42+
43+// ReleaseUsage stores release metadata, not object contents.
44+message ReleaseUsage {
45+ int64 used = 1;
46+}
@@ -0,0 +1,46 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+syntax = "proto3";
4+package ermite.storage;
5+import "google/protobuf/timestamp.proto";
6+option go_package = "bnema.dev/ermite/gen/ermite/storage;storagepb";
7+
8+// Release stores release metadata, not object contents.
9+message Release {
10+ int64 id = 1;
11+ string repo = 2;
12+ string tag = 3;
13+ string title = 4;
14+ string notes = 5;
15+ bool draft = 6;
16+ bool prerelease = 7;
17+ bool make_latest = 8;
18+ string target_commitish = 9;
19+ string author = 10;
20+ google.protobuf.Timestamp created_at = 11;
21+ google.protobuf.Timestamp published_at = 12;
22+ google.protobuf.Timestamp updated_at = 13;
23+}
24+
25+// ReleaseAsset stores release metadata, not object contents.
26+message ReleaseAsset {
27+ int64 id = 1;
28+ int64 release_id = 2;
29+ string repo = 3;
30+ string name = 4;
31+ string label = 5;
32+ string content_type = 6;
33+ int64 size = 7;
34+ string digest = 8;
35+ string state = 9;
36+ string uploader = 10;
37+ int64 charged = 11;
38+ string multipart_id = 12;
39+ google.protobuf.Timestamp created_at = 13;
40+ google.protobuf.Timestamp updated_at = 14;
41+}
42+
43+// ReleaseUsage stores release metadata, not object contents.
44+message ReleaseUsage {
45+ int64 used = 1;
46+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 42

↑↓ moveenter openesc close