release.gno
4.69 Kb · 162 lines
1package forge
2
3import "strings"
4
5// Size caps for a release.
6const (
7 MaxTagLen = 64
8 MaxRelNameLen = 200
9 MaxNotesLen = 8192
10)
11
12// Release is a named point in a repo's history, with notes.
13//
14// A tag ref already says "this name points at this object". A release says why
15// anyone should care: what changed, whether it is ready, and which deployed
16// package paths came out of it. That last part is the one an explorer wants,
17// and it is also the only honest bridge between a git object id and an on-chain
18// deploy, because the chain cannot verify either against the other.
19type Release struct {
20 Tag string // "v1.2.0", the key; immutable once cut
21 Name string // human title, may be empty
22 Notes string // markdown, may be empty
23 OID string // the object this release names
24 Ref string // the tag ref, "" if the release does not have one
25 Prerelease bool
26 CreatedAt int64 // block height
27 CreatedBy address
28 Yanked bool // withdrawn; kept, because a version that existed cannot un-exist
29 YankReason string
30}
31
32// ValidTag reports whether s is usable as a release tag.
33//
34// Not semver-enforcing: gno.land's own packages version as v0, v1 with no minor
35// or patch, and a forge that refused those would refuse its own ecosystem.
36// Ordering is therefore lexical over a padded form, not semantic, and
37// LatestRelease says so.
38func ValidTag(s string) bool {
39 if s == "" || len(s) > MaxTagLen {
40 return false
41 }
42 if strings.Contains(s, "..") || strings.Contains(s, "/") {
43 return false
44 }
45 for i := 0; i < len(s); i++ {
46 c := s[i]
47 ok := (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
48 (c >= '0' && c <= '9') || c == '.' || c == '-' || c == '_' || c == '+'
49 if !ok {
50 return false
51 }
52 }
53 return true
54}
55
56// CutRelease records a release. The tag is the key and cannot be re-cut: a
57// consumer that pinned v1.2.0 and got different bytes later has been lied to,
58// and the whole point of a tag is that it does not move. Withdrawing one is
59// YankRelease, which leaves it visible.
60func (r *Repo) CutRelease(actor address, height int64, tag, name, notes, oid, ref string, prerelease bool) (*Release, error) {
61 if r.Archived {
62 return nil, ErrRepoArchived
63 }
64 if !r.Can(actor, RoleWriter) {
65 return nil, ErrUnauthorized
66 }
67 if !ValidTag(tag) {
68 return nil, ErrInvalidTag
69 }
70 if !ValidText(name, MaxRelNameLen) || !ValidText(notes, MaxNotesLen) {
71 return nil, ErrInvalidText
72 }
73 if oid != "" && !ValidOID(oid) {
74 return nil, ErrInvalidOID
75 }
76 if ref != "" && !ValidRefName(ref) {
77 return nil, ErrInvalidRefName
78 }
79 if r.releases.Has(tag) {
80 return nil, ErrReleaseExists
81 }
82
83 rel := &Release{
84 Tag: tag,
85 Name: name,
86 Notes: notes,
87 OID: oid,
88 Ref: ref,
89 Prerelease: prerelease,
90 CreatedAt: height,
91 CreatedBy: actor,
92 }
93 r.releases.Set(tag, rel)
94 r.releaseOrder.Set(seqKey(int64(r.releases.Size())), tag)
95 return rel, nil
96}
97
98// YankRelease marks a release withdrawn without deleting it.
99//
100// Deleting would make a pinned version silently vanish, which reads to a
101// consumer as "this was never published" rather than "do not use this".
102func (r *Repo) YankRelease(actor address, tag, reason string) error {
103 if !r.Can(actor, RoleWriter) {
104 return ErrUnauthorized
105 }
106 if !ValidText(reason, MaxNoteLen) {
107 return ErrInvalidText
108 }
109 v := r.releases.Get(tag)
110 if v == nil {
111 return ErrReleaseNotFound
112 }
113 rel := v.(*Release)
114 rel.Yanked = true
115 rel.YankReason = reason
116 return nil
117}
118
119// Release returns one release by tag, or nil.
120func (r *Repo) Release(tag string) *Release {
121 v := r.releases.Get(tag)
122 if v == nil {
123 return nil
124 }
125 return v.(*Release)
126}
127
128// ReleaseCount is how many releases this repo has cut, yanked ones included.
129func (r *Repo) ReleaseCount() int { return r.releases.Size() }
130
131// IterateReleases walks releases newest-cut first.
132//
133// Cut order, not tag order: tags are not required to be semver here (gno.land's
134// own packages are v0 and v1), so there is no total order to sort by that would
135// not be wrong for somebody. When they were cut is a fact the forge does know.
136func (r *Repo) IterateReleases(offset, count int, cb func(*Release) bool) {
137 if count <= 0 {
138 count = r.releaseOrder.Size()
139 }
140 r.releaseOrder.ReverseIterateByOffset(offset, count, func(_ string, v any) bool {
141 rel := r.releases.Get(v.(string))
142 if rel == nil {
143 return false
144 }
145 return cb(rel.(*Release))
146 })
147}
148
149// LatestRelease is the most recently cut release that is neither a prerelease
150// nor yanked, or nil. "Most recently cut", never "highest version": see
151// IterateReleases.
152func (r *Repo) LatestRelease() *Release {
153 var out *Release
154 r.IterateReleases(0, 0, func(rel *Release) bool {
155 if rel.Prerelease || rel.Yanked {
156 return false
157 }
158 out = rel
159 return true
160 })
161 return out
162}