Commit 0f4f9b4c10
0f4f9b4c106d2e6f2ba93df83ff3653faead0e8b
parent: ca02f30bc0
Verified · cmc ci/build: success ci/sonar: success ci/test: success ci/vuln: success
cmc <hello@cleberg.net> · 2026-09-06 15:02 UTC
control, web, wiki: render dependency status on the settings page
repo deps status was CLI-only, so the toggle was visible and its output
was not. Its payload becomes a named type the settings page decodes: last
check, last error, the tracking issue, and what is behind.
Closes #164
Layout: unified · split
.gitbay/wiki/Parity.org
+5 −4
| @@ -146,7 +146,7 @@ format column and are always markdown. |
| 146 | 146 | | build trigger | yes | yes | yes | |
| 147 | 147 | | build cancel | yes | no | no | |
| 148 | 148 | | dependency checks on/off | yes | yes | no | |
| 149 | | | dependency status | yes | no | no | |
| 149 | | dependency status | yes | yes | no | |
| 150 | 150 | | delete, transfer | yes | no | no | |
| 151 | 151 | | release delete | yes | yes | no | |
| 152 | 152 | |
| @@ -163,9 +163,10 @@ on the build page yet. |
| 163 | 163 | |
| 164 | 164 | Dependency checks are off until a repository's admin turns them on: the |
| 165 | 165 | check tells a public registry what the repository depends on. =repo deps |
| 166 | | status= lists what is behind and has no web view because the report |
| 167 | | itself is an issue the worker opens, rewrites and closes, which every |
| 168 | | surface already reads. |
| 166 | status= lists what is behind; the repository's settings page renders the |
| 167 | same report under the toggle — last check, last error, the tracking |
| 168 | issue, and the rows. The issue the worker opens, rewrites and closes is |
| 169 | still the copy every other surface reads. |
| 169 | 170 | |
| 170 | 171 | The wiki row covers reading. A wiki lives at =.gitbay/wiki/= on the |
| 171 | 172 | default branch, so editing one is editing a file in the repository — |
e2e/deps_test.go
+8 −1
| @@ -20,7 +20,7 @@ type depsStatus struct { |
| 20 | 20 | // can turn it on: the check tells a public registry what the repository |
| 21 | 21 | // depends on. |
| 22 | 22 | func TestDepsEnableDisable(t *testing.T) { |
| 23 | | inst := startInstance(t) |
| 23 | inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n") |
| 24 | 24 | aliceKey := inst.newKey(t, "alice") |
| 25 | 25 | inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub", |
| 26 | 26 | "--email", "alice@example.test", "--verified") |
| @@ -72,6 +72,13 @@ func TestDepsEnableDisable(t *testing.T) { |
| 72 | 72 | t.Fatalf("second deps enable: %s", errOut) |
| 73 | 73 | } |
| 74 | 74 | |
| 75 | // The settings page reports the same state the command does (#164). |
| 76 | alice := inst.login(t, aliceKey) |
| 77 | _, page := browserGet(t, alice, inst.base()+"/alice/app/settings") |
| 78 | if !strings.Contains(page, "Last checked") || !strings.Contains(page, "Nothing behind") { |
| 79 | t.Errorf("settings page does not report the check state:\n%s", page) |
| 80 | } |
| 81 | |
| 75 | 82 | if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "deps", "disable", "alice/app"); code != 0 { |
| 76 | 83 | t.Fatalf("deps disable: %s", errOut) |
| 77 | 84 | } |
internal/control/deps.go
+22 −15
| @@ -25,6 +25,25 @@ func init() { |
| 25 | 25 | Usage: "repo deps status <owner/name>", ReadOnly: true, Run: runDepsStatus}) |
| 26 | 26 | } |
| 27 | 27 | |
| 28 | // DepsOut is what `repo deps status` emits: the check's state and what |
| 29 | // the last run found behind. Named so the web decodes the same struct |
| 30 | // the command writes. |
| 31 | type DepsOut struct { |
| 32 | Enabled bool `json:"enabled"` |
| 33 | LastCheck string `json:"last_check,omitempty"` |
| 34 | LastError string `json:"last_error,omitempty"` |
| 35 | IssueNumber int64 `json:"issue_number,omitempty"` |
| 36 | Behind []DepBehind `json:"behind"` |
| 37 | } |
| 38 | |
| 39 | // DepBehind is one dependency with a newer release than the manifest pins. |
| 40 | type DepBehind struct { |
| 41 | Ecosystem string `json:"ecosystem"` |
| 42 | Name string `json:"name"` |
| 43 | Current string `json:"current"` |
| 44 | Latest string `json:"latest"` |
| 45 | } |
| 46 | |
| 28 | 47 | func runDepsEnable(c *Ctx, args []string) int { |
| 29 | 48 | if len(args) != 1 { |
| 30 | 49 | return c.fail(protocol.ExitUsage, "usage: repo deps enable <owner/name>") |
| @@ -78,22 +97,10 @@ func runDepsStatus(c *Ctx, args []string) int { |
| 78 | 97 | if err != nil { |
| 79 | 98 | return c.fail(protocol.ExitFailure, "%v", err) |
| 80 | 99 | } |
| 81 | | type behind struct { |
| 82 | | Ecosystem string `json:"ecosystem"` |
| 83 | | Name string `json:"name"` |
| 84 | | Current string `json:"current"` |
| 85 | | Latest string `json:"latest"` |
| 86 | | } |
| 87 | | out := struct { |
| 88 | | Enabled bool `json:"enabled"` |
| 89 | | LastCheck string `json:"last_check,omitempty"` |
| 90 | | LastError string `json:"last_error,omitempty"` |
| 91 | | IssueNumber int64 `json:"issue_number,omitempty"` |
| 92 | | Behind []behind `json:"behind"` |
| 93 | | }{Enabled: true, LastCheck: check.LastCheck, LastError: check.LastError, |
| 94 | | IssueNumber: check.IssueNumber, Behind: []behind{}} |
| 100 | out := DepsOut{Enabled: true, LastCheck: check.LastCheck, LastError: check.LastError, |
| 101 | IssueNumber: check.IssueNumber, Behind: []DepBehind{}} |
| 95 | 102 | for _, r := range reports { |
| 96 | | out.Behind = append(out.Behind, behind{r.Ecosystem, r.Name, r.Current, r.Latest}) |
| 103 | out.Behind = append(out.Behind, DepBehind{r.Ecosystem, r.Name, r.Current, r.Latest}) |
| 97 | 104 | } |
| 98 | 105 | return c.emit(out, func(w io.Writer) { |
| 99 | 106 | fmt.Fprintf(w, "checks on, last %s\n", orDash(check.LastCheck)) |
internal/control/output_test.go
+2 −2
| @@ -16,7 +16,7 @@ func TestNamedPayloadsRoundTrip(t *testing.T) { |
| 16 | 16 | &Created{}, &MRCreated{}, &IssueShow{}, &MRShow{}, |
| 17 | 17 | &BuildOut{}, &JobOut{}, &ProfileOut{}, &ProfileRepo{}, &ProfileMember{}, |
| 18 | 18 | &DashboardOut{}, &DashboardItem{}, &DashboardBuild{}, &PinnedOut{}, |
| 19 | | &FeedOut{}, &ActivityDay{}, &SearchResult{}, &ReviewOut{}, &CheckOut{}, &CommitOut{}, |
| 19 | &FeedOut{}, &ActivityDay{}, &DepsOut{}, &DepBehind{}, &SearchResult{}, &ReviewOut{}, &CheckOut{}, &CommitOut{}, |
| 20 | 20 | } |
| 21 | 21 | for _, p := range payloads { |
| 22 | 22 | name := reflect.TypeOf(p).Elem().Name() |
| @@ -39,7 +39,7 @@ func TestPayloadFieldsAreTagged(t *testing.T) { |
| 39 | 39 | types := []any{ |
| 40 | 40 | Created{}, MRCreated{}, BuildOut{}, JobOut{}, ProfileOut{}, ProfileRepo{}, |
| 41 | 41 | ProfileMember{}, DashboardOut{}, DashboardItem{}, DashboardBuild{}, |
| 42 | | PinnedOut{}, FeedOut{}, ActivityDay{}, SearchResult{}, ReviewOut{}, |
| 42 | PinnedOut{}, FeedOut{}, ActivityDay{}, DepsOut{}, DepBehind{}, SearchResult{}, ReviewOut{}, |
| 43 | 43 | CheckOut{}, CommitOut{}, ServerOut{}, |
| 44 | 44 | } |
| 45 | 45 | for _, v := range types { |
internal/httpd/depspage_test.go
added
+48
| @@ -0,0 +1,48 @@ |
| 1 | package httpd |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | |
| 7 | "gitbay.org/gitbay/internal/control" |
| 8 | "gitbay.org/gitbay/internal/web" |
| 9 | ) |
| 10 | |
| 11 | // The settings page renders what `repo deps status` reports, not just the |
| 12 | // toggle (#164). A renamed field would be a blank cell rather than a |
| 13 | // compile error, so render the page and look for the values. |
| 14 | func TestSettingsPageRendersDepsStatus(t *testing.T) { |
| 15 | var sb strings.Builder |
| 16 | err := web.Render(&sb, "settings.html", settingsPage{ |
| 17 | repoPage: testRepoPage(), |
| 18 | DepsEnabled: true, |
| 19 | Deps: control.DepsOut{ |
| 20 | Enabled: true, LastCheck: "2026-09-03T08:20:25Z", IssueNumber: 140, |
| 21 | Behind: []control.DepBehind{{Ecosystem: "go", Name: "golang.org/x/crypto", |
| 22 | Current: "v0.31.0", Latest: "v0.42.0"}}, |
| 23 | }, |
| 24 | }) |
| 25 | if err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | page := sb.String() |
| 29 | for _, want := range []string{ |
| 30 | "2026-09-03T08:20:25Z", "golang.org/x/crypto", "v0.31.0", "v0.42.0", |
| 31 | `href="/krz/gitbay/issues/140"`, |
| 32 | } { |
| 33 | if !strings.Contains(page, want) { |
| 34 | t.Errorf("settings page does not show %q", want) |
| 35 | } |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | // With checks off, none of the report shows. |
| 40 | func TestSettingsPageHidesDepsStatusWhenOff(t *testing.T) { |
| 41 | var sb strings.Builder |
| 42 | if err := web.Render(&sb, "settings.html", settingsPage{repoPage: testRepoPage()}); err != nil { |
| 43 | t.Fatal(err) |
| 44 | } |
| 45 | if strings.Contains(sb.String(), "Last checked") { |
| 46 | t.Error("the report shows with checks off") |
| 47 | } |
| 48 | } |
internal/httpd/settings.go
+9 −3
| @@ -5,6 +5,7 @@ import ( |
| 5 | 5 | "net/http" |
| 6 | 6 | "strings" |
| 7 | 7 | |
| 8 | "gitbay.org/gitbay/internal/control" |
| 8 | 9 | "gitbay.org/gitbay/internal/gitutil" |
| 9 | 10 | "gitbay.org/gitbay/internal/store" |
| 10 | 11 | ) |
| @@ -19,6 +20,7 @@ type settingsPage struct { |
| 19 | 20 | Topics []string |
| 20 | 21 | Branches []gitutil.Ref |
| 21 | 22 | DepsEnabled bool |
| 23 | Deps control.DepsOut |
| 22 | 24 | Notice string |
| 23 | 25 | } |
| 24 | 26 | |
| @@ -34,11 +36,15 @@ func (s *Server) settingsForm(w http.ResponseWriter, r *http.Request, u store.Us |
| 34 | 36 | p.Tab = "settings" |
| 35 | 37 | topics, _ := s.st.ListTopics(repo.ID) |
| 36 | 38 | branches, _ := gitutil.Refs(p.Dir, "heads") |
| 37 | | _, depsErr := s.st.DepCheckFor(repo.ID) |
| 39 | // The toggle's state comes from the store; what the last run found |
| 40 | // comes from the command, so the page shows the same report the CLI |
| 41 | // prints (#164). |
| 42 | var deps control.DepsOut |
| 43 | s.runControlInto(u, []string{"repo", "deps", "status", repo.Path()}, &deps) |
| 38 | 44 | s.render(w, "settings.html", settingsPage{ |
| 39 | 45 | repoPage: p, Topics: topics, Branches: branches, |
| 40 | | DepsEnabled: depsErr == nil, |
| 41 | | Notice: s.takeFlash(w, r), |
| 46 | DepsEnabled: deps.Enabled, Deps: deps, |
| 47 | Notice: s.takeFlash(w, r), |
| 42 | 48 | }) |
| 43 | 49 | } |
| 44 | 50 | |
internal/web/templates/settings.html
+13
| @@ -109,6 +109,19 @@ |
| 109 | 109 | proxy.golang.org, npm, crates.io, and PyPI once a day, and tracks what is behind |
| 110 | 110 | in an issue. Checking a private repository tells those registries what it |
| 111 | 111 | depends on.</p> |
| 112 | {{if .DepsEnabled}} |
| 113 | <p class="meta">Last checked {{if .Deps.LastCheck}}{{.Deps.LastCheck}}{{else}}never{{end}}{{if .Deps.IssueNumber}} · tracked in <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/issues/{{.Deps.IssueNumber}}">#{{.Deps.IssueNumber}}</a>{{end}}</p> |
| 114 | {{if .Deps.LastError}}<p class="error" role="alert">{{.Deps.LastError}}</p>{{end}} |
| 115 | {{if .Deps.Behind}}<div class="tablewrap"><table class="keys"> |
| 116 | <tr class="cols"><th scope="col">dependency</th><th scope="col">pinned</th><th scope="col">latest</th></tr> |
| 117 | {{range .Deps.Behind}}<tr> |
| 118 | <td class="mono">{{.Ecosystem}} {{.Name}}</td> |
| 119 | <td class="mono">{{.Current}}</td> |
| 120 | <td class="mono">{{.Latest}}</td> |
| 121 | </tr> |
| 122 | {{end}}</table></div> |
| 123 | {{else}}<p class="none">Nothing behind{{if not .Deps.LastCheck}} — the first check has not run yet{{end}}.</p>{{end}} |
| 124 | {{end}} |
| 112 | 125 | |
| 113 | 126 | <h2>Lifecycle</h2> |
| 114 | 127 | <form method="post" action="{{$base}}" class="setform"> |