Commit 70dc0648f9

70dc0648f931f6f7112c6b71b0be485eeae4077f

parent: 19682e7e50

Verified · cmc ci/build: success ci/sonar: success ci/test: success ci/vuln: success

cmc <hello@cleberg.net> · 2026-09-06 14:53 UTC

web, wiki: delete a release from the browser

release delete was CLI-only. The releases page offers it to a repo admin
inside the edit disclosure, dispatching the same command with --yes.

Closes #165

Layout: unified · split

.gitbay/wiki/Parity.org +1 −1
@@ -147,7 +147,7 @@ format column and are always markdown.
147147| dependency checks on/off | yes | yes | no |
148148| dependency status | yes | no | no |
149149| delete, transfer | yes | no | no |
150| release delete | yes | no | no |
150| release delete | yes | yes | no |
151151
152152No row is web-only any more. Blame, file editing, log at a ref,
153153archive, the public listing and the wiki were all in that state — a
e2e/releaseweb_test.go +20 −2
@@ -88,7 +88,25 @@ func TestReleaseAndBuildWeb(t *testing.T) {
8888 if _, p := browserGet(t, bob, base+"/builds"); strings.Contains(p, "Run a job now") {
8989 t.Fatal("reader sees the trigger control")
9090 }
91 if _, p := browserGet(t, bob, base+"/releases"); strings.Contains(p, "New release") {
92 t.Fatal("reader sees the release form")
91 if _, p := browserGet(t, bob, base+"/releases"); strings.Contains(p, "New release") ||
92 strings.Contains(p, "Delete release") {
93 t.Fatal("reader sees a release control")
94 }
95
96 // Delete is offered to a repo admin and removes the release; the tag
97 // survives, so it is offered for creation again (#165).
98 if _, p := browserGet(t, alice, base+"/releases"); !strings.Contains(p, "Delete release") {
99 t.Fatalf("owner is not offered the delete control:\n%s", p)
100 }
101 if status, _ := browserPost(t, alice, base+"/releases", url.Values{
102 "action": {"delete"}, "tag": {"v1.0"}}); status != 200 {
103 t.Fatal("release delete failed")
104 }
105 out, _, _ = inst.ssh(t, aliceKey, "", "release", "list", "alice/app", "--json")
106 if strings.Contains(out, "v1.0") {
107 t.Fatalf("release still listed after delete:\n%s", out)
108 }
109 if _, p := browserGet(t, alice, base+"/releases"); !strings.Contains(p, `<option value="v1.0"`) {
110 t.Fatalf("tag not free again after delete:\n%s", p)
93111 }
94112}
internal/httpd/releaseactions.go +10 −1
@@ -27,6 +27,15 @@ func (s *Server) releaseSubmit(w http.ResponseWriter, r *http.Request, u store.U
2727 s.backTo(w, r, "releases", "pick a tag")
2828 return
2929 }
30 back := func(w http.ResponseWriter, r *http.Request, msg string) { s.backTo(w, r, "releases", msg) }
31 // The CLI's --yes guards against a mistyped tag; here the tag comes from
32 // the page and the button sits behind a disclosure, so the click is the
33 // deliberate act.
34 if r.FormValue("action") == "delete" {
35 _, msg, code := s.runControlCode(u, []string{"release", "delete", repo, tag, "--yes"})
36 s.done(w, r, code, msg, back)
37 return
38 }
3039 verb := "create"
3140 if r.FormValue("action") == "edit" {
3241 verb = "edit"
@@ -40,7 +49,7 @@ func (s *Server) releaseSubmit(w http.ResponseWriter, r *http.Request, u store.U
4049 argv = append(argv, "--notes", notes)
4150 }
4251 _, msg, code := s.runControlCode(u, argv)
43 s.done(w, r, code, msg, func(w http.ResponseWriter, r *http.Request, msg string) { s.backTo(w, r, "releases", msg) })
52 s.done(w, r, code, msg, back)
4453}
4554
4655func (s *Server) buildTriggerSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
internal/web/templates/releases.html +7 −1
@@ -31,7 +31,13 @@
3131 <p><input type="text" name="title" aria-label="Title" value="{{$rel.Title}}"></p>
3232 <p><textarea name="notes" aria-label="Notes" rows="6">{{$rel.Notes}}</textarea></p>
3333 <p><button type="submit">Save</button></p>
34 </form></details>{{end}}
34 </form>
35 {{if $.CanAdmin}}<form method="post" action="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/releases" class="commentform">
36 <input type="hidden" name="action" value="delete">
37 <input type="hidden" name="tag" value="{{$rel.Tag}}">
38 <p class="meta">Deleting is permanent and takes the assets with it. The tag stays.</p>
39 <p><button type="submit" class="linklike">Delete release</button></p>
40 </form>{{end}}</details>{{end}}
3541 {{if $rel.Assets}}<table class="assets">
3642 <thead><tr><th scope="col">File</th><th scope="col">Bytes</th><th scope="col">SHA-256</th></tr></thead>
3743 {{range $rel.Assets}}<tr>