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. |
| 147 | 147 | | dependency checks on/off | yes | yes | no | |
| 148 | 148 | | dependency status | yes | no | no | |
| 149 | 149 | | delete, transfer | yes | no | no | |
| 150 | | | release delete | yes | no | no | |
| 150 | | release delete | yes | yes | no | |
| 151 | 151 | |
| 152 | 152 | No row is web-only any more. Blame, file editing, log at a ref, |
| 153 | 153 | archive, 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) { |
| 88 | 88 | if _, p := browserGet(t, bob, base+"/builds"); strings.Contains(p, "Run a job now") { |
| 89 | 89 | t.Fatal("reader sees the trigger control") |
| 90 | 90 | } |
| 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) |
| 93 | 111 | } |
| 94 | 112 | } |
internal/httpd/releaseactions.go
+10 −1
| @@ -27,6 +27,15 @@ func (s *Server) releaseSubmit(w http.ResponseWriter, r *http.Request, u store.U |
| 27 | 27 | s.backTo(w, r, "releases", "pick a tag") |
| 28 | 28 | return |
| 29 | 29 | } |
| 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 | } |
| 30 | 39 | verb := "create" |
| 31 | 40 | if r.FormValue("action") == "edit" { |
| 32 | 41 | verb = "edit" |
| @@ -40,7 +49,7 @@ func (s *Server) releaseSubmit(w http.ResponseWriter, r *http.Request, u store.U |
| 40 | 49 | argv = append(argv, "--notes", notes) |
| 41 | 50 | } |
| 42 | 51 | _, 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) |
| 44 | 53 | } |
| 45 | 54 | |
| 46 | 55 | func (s *Server) buildTriggerSubmit(w http.ResponseWriter, r *http.Request, u store.User) { |
internal/web/templates/releases.html
+7 −1
| @@ -31,7 +31,13 @@ |
| 31 | 31 | <p><input type="text" name="title" aria-label="Title" value="{{$rel.Title}}"></p> |
| 32 | 32 | <p><textarea name="notes" aria-label="Notes" rows="6">{{$rel.Notes}}</textarea></p> |
| 33 | 33 | <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}} |
| 35 | 41 | {{if $rel.Assets}}<table class="assets"> |
| 36 | 42 | <thead><tr><th scope="col">File</th><th scope="col">Bytes</th><th scope="col">SHA-256</th></tr></thead> |
| 37 | 43 | {{range $rel.Assets}}<tr> |