feat(web): group the checks of a merge request by run when a commit has several
8 files · +213 -45
M internal/adapters/web/actions.templ +3 -0
| @@ -456,6 +456,9 @@ templ MRChecks(c ChecksSection) { | |||
| 456 | 456 | } else { | |
| 457 | 457 | <ul class="checks"> | |
| 458 | 458 | for _, r := range c.Rows { | |
| 459 | + | if r.GroupLabel != "" { | |
| 460 | + | <li class="ck-group"><a href={ templ.SafeURL(r.GroupHref) }>{ r.GroupLabel }</a></li> | |
| 461 | + | } | |
| 459 | 462 | <li> | |
| 460 | 463 | @CheckIcon(r.Kind, r.State) | |
| 461 | 464 | if r.Href != "" { | |
| @@ -456,6 +456,9 @@ templ MRChecks(c ChecksSection) { | |||||
| 456 | } else { | 456 | } else { | ||
| 457 | <ul class="checks"> | 457 | <ul class="checks"> | ||
| 458 | for _, r := range c.Rows { | 458 | for _, r := range c.Rows { | ||
| 459 | + | if r.GroupLabel != "" { | |||
| 460 | + | <li class="ck-group"><a href={ templ.SafeURL(r.GroupHref) }>{ r.GroupLabel }</a></li> | |||
| 461 | + | } | |||
| 459 | <li> | 462 | <li> | ||
| 460 | @CheckIcon(r.Kind, r.State) | 463 | @CheckIcon(r.Kind, r.State) | ||
| 461 | if r.Href != "" { | 464 | if r.Href != "" { | ||
M internal/adapters/web/actions_templ.go +66 -34 generated file: collapsed
M internal/adapters/web/checks.go +38 -10
| @@ -25,6 +25,7 @@ type ChecksSummary struct { | |||
| 25 | 25 | type CheckRow struct { | |
| 26 | 26 | Name, Kind, State, Href string | |
| 27 | 27 | Waiting bool | |
| 28 | + | GroupLabel, GroupHref string | |
| 28 | 29 | } | |
| 29 | 30 | ||
| 30 | 31 | // ChecksSection is the checks of the head of a merge request. | |
| @@ -82,17 +83,28 @@ func checksSummary(repo domain.RepoName, sha string, checks []ports.CheckView) C | |||
| 82 | 83 | // the repository that applies to them. | |
| 83 | 84 | func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection { | |
| 84 | 85 | sec := ChecksSection{} | |
| 85 | - | for _, c := range checks { | |
| 86 | - | row := CheckRow{ | |
| 87 | - | Name: visibleText(c.Name), Kind: ciKind(c.Status, c.Conclusion), State: ciState(c.Status, c.Conclusion), | |
| 88 | - | Waiting: c.WaitingForRunner, | |
| 89 | - | } | |
| 90 | - | if c.JobID != "" { | |
| 91 | - | row.Href = jobHref(repo.Name.String(), c.RunID, c.JobID) | |
| 92 | - | } else { | |
| 93 | - | row.Href = runHref(repo.Name.String(), c.RunID) | |
| 86 | + | groups := checksByRun(checks) | |
| 87 | + | for _, group := range groups { | |
| 88 | + | for i, c := range group { | |
| 89 | + | row := CheckRow{ | |
| 90 | + | Name: visibleText(c.Name), Kind: ciKind(c.Status, c.Conclusion), State: ciState(c.Status, c.Conclusion), | |
| 91 | + | Waiting: c.WaitingForRunner, | |
| 92 | + | } | |
| 93 | + | if c.JobID != "" { | |
| 94 | + | row.Href = jobHref(repo.Name.String(), c.RunID, c.JobID) | |
| 95 | + | } else { | |
| 96 | + | row.Href = runHref(repo.Name.String(), c.RunID) | |
| 97 | + | } | |
| 98 | + | if len(groups) > 1 && i == 0 { | |
| 99 | + | event := visibleText(c.Event) | |
| 100 | + | if event == "" { | |
| 101 | + | event = fmt.Sprintf("run %d", c.RunID) | |
| 102 | + | } | |
| 103 | + | row.GroupLabel = visibleText(c.WorkflowName) + " · " + event | |
| 104 | + | row.GroupHref = runHref(repo.Name.String(), c.RunID) | |
| 105 | + | } | |
| 106 | + | sec.Rows = append(sec.Rows, row) | |
| 94 | 107 | } | |
| 95 | - | sec.Rows = append(sec.Rows, row) | |
| 96 | 108 | } | |
| 97 | 109 | switch { | |
| 98 | 110 | case len(checks) == 0: | |
| @@ -104,3 +116,19 @@ func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection { | |||
| 104 | 116 | } | |
| 105 | 117 | return sec | |
| 106 | 118 | } | |
| 119 | + | ||
| 120 | + | // checksByRun keeps first-appearance order and the check order within each run. | |
| 121 | + | func checksByRun(checks []ports.CheckView) [][]ports.CheckView { | |
| 122 | + | groups := make([][]ports.CheckView, 0) | |
| 123 | + | indices := make(map[int64]int) | |
| 124 | + | for _, c := range checks { | |
| 125 | + | i, ok := indices[c.RunID] | |
| 126 | + | if !ok { | |
| 127 | + | i = len(groups) | |
| 128 | + | indices[c.RunID] = i | |
| 129 | + | groups = append(groups, nil) | |
| 130 | + | } | |
| 131 | + | groups[i] = append(groups[i], c) | |
| 132 | + | } | |
| 133 | + | return groups | |
| 134 | + | } | |
| @@ -25,6 +25,7 @@ type ChecksSummary struct { | |||||
| 25 | type CheckRow struct { | 25 | type CheckRow struct { | ||
| 26 | Name, Kind, State, Href string | 26 | Name, Kind, State, Href string | ||
| 27 | Waiting bool | 27 | Waiting bool | ||
| 28 | + | GroupLabel, GroupHref string | |||
| 28 | } | 29 | } | ||
| 29 | 30 | ||||
| 30 | // ChecksSection is the checks of the head of a merge request. | 31 | // ChecksSection is the checks of the head of a merge request. | ||
| @@ -82,17 +83,28 @@ func checksSummary(repo domain.RepoName, sha string, checks []ports.CheckView) C | |||||
| 82 | // the repository that applies to them. | 83 | // the repository that applies to them. | ||
| 83 | func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection { | 84 | func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection { | ||
| 84 | sec := ChecksSection{} | 85 | sec := ChecksSection{} | ||
| 85 | - | for _, c := range checks { | 86 | + | groups := checksByRun(checks) |
| 86 | - | row := CheckRow{ | 87 | + | for _, group := range groups { |
| 87 | - | Name: visibleText(c.Name), Kind: ciKind(c.Status, c.Conclusion), State: ciState(c.Status, c.Conclusion), | 88 | + | for i, c := range group { |
| 88 | - | Waiting: c.WaitingForRunner, | 89 | + | row := CheckRow{ |
| 89 | - | } | 90 | + | Name: visibleText(c.Name), Kind: ciKind(c.Status, c.Conclusion), State: ciState(c.Status, c.Conclusion), |
| 90 | - | if c.JobID != "" { | 91 | + | Waiting: c.WaitingForRunner, |
| 91 | - | row.Href = jobHref(repo.Name.String(), c.RunID, c.JobID) | 92 | + | } |
| 92 | - | } else { | 93 | + | if c.JobID != "" { |
| 93 | - | row.Href = runHref(repo.Name.String(), c.RunID) | 94 | + | row.Href = jobHref(repo.Name.String(), c.RunID, c.JobID) |
| 95 | + | } else { | |||
| 96 | + | row.Href = runHref(repo.Name.String(), c.RunID) | |||
| 97 | + | } | |||
| 98 | + | if len(groups) > 1 && i == 0 { | |||
| 99 | + | event := visibleText(c.Event) | |||
| 100 | + | if event == "" { | |||
| 101 | + | event = fmt.Sprintf("run %d", c.RunID) | |||
| 102 | + | } | |||
| 103 | + | row.GroupLabel = visibleText(c.WorkflowName) + " · " + event | |||
| 104 | + | row.GroupHref = runHref(repo.Name.String(), c.RunID) | |||
| 105 | + | } | |||
| 106 | + | sec.Rows = append(sec.Rows, row) | |||
| 94 | } | 107 | } | ||
| 95 | - | sec.Rows = append(sec.Rows, row) | |||
| 96 | } | 108 | } | ||
| 97 | switch { | 109 | switch { | ||
| 98 | case len(checks) == 0: | 110 | case len(checks) == 0: | ||
| @@ -104,3 +116,19 @@ func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection { | |||||
| 104 | } | 116 | } | ||
| 105 | return sec | 117 | return sec | ||
| 106 | } | 118 | } | ||
| 119 | + | ||||
| 120 | + | // checksByRun keeps first-appearance order and the check order within each run. | |||
| 121 | + | func checksByRun(checks []ports.CheckView) [][]ports.CheckView { | |||
| 122 | + | groups := make([][]ports.CheckView, 0) | |||
| 123 | + | indices := make(map[int64]int) | |||
| 124 | + | for _, c := range checks { | |||
| 125 | + | i, ok := indices[c.RunID] | |||
| 126 | + | if !ok { | |||
| 127 | + | i = len(groups) | |||
| 128 | + | indices[c.RunID] = i | |||
| 129 | + | groups = append(groups, nil) | |||
| 130 | + | } | |||
| 131 | + | groups[i] = append(groups[i], c) | |||
| 132 | + | } | |||
| 133 | + | return groups | |||
| 134 | + | } | |||
A internal/adapters/web/checks_test.go +49 -0
| @@ -0,0 +1,49 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package web | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bytes" | |
| 7 | + | "context" | |
| 8 | + | "testing" | |
| 9 | + | ||
| 10 | + | "github.com/stretchr/testify/assert" | |
| 11 | + | "github.com/stretchr/testify/require" | |
| 12 | + | ||
| 13 | + | "bnema.dev/ermite/internal/domain" | |
| 14 | + | "bnema.dev/ermite/internal/ports" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | func TestChecksSectionRuns(t *testing.T) { | |
| 18 | + | t.Parallel() | |
| 19 | + | repo := domain.Repo{Name: repoName(t, "proj")} | |
| 20 | + | check := func(run int64, name, event string) ports.CheckView { | |
| 21 | + | return ports.CheckView{CICheck: domain.CICheck{RunID: run, Name: name, WorkflowName: "Smoke", Status: domain.CICompleted, Conclusion: domain.CISuccess}, Event: event} | |
| 22 | + | } | |
| 23 | + | a, b, c := check(7, "build", "push"), check(8, "build", "pull_request"), check(7, "test", "push") | |
| 24 | + | single := checksSection(repo, []ports.CheckView{a, c}) | |
| 25 | + | require.Len(t, single.Rows, 2) | |
| 26 | + | assert.Equal(t, []CheckRow{ | |
| 27 | + | {Name: "build", Kind: "ok", State: "success", Href: runHref("proj", 7)}, | |
| 28 | + | {Name: "test", Kind: "ok", State: "success", Href: runHref("proj", 7)}, | |
| 29 | + | }, single.Rows) | |
| 30 | + | var flat bytes.Buffer | |
| 31 | + | require.NoError(t, MRChecks(single).Render(context.Background(), &flat)) | |
| 32 | + | assert.NotContains(t, flat.String(), "ck-group") | |
| 33 | + | ||
| 34 | + | multi := checksSection(repo, []ports.CheckView{a, b, c}) | |
| 35 | + | require.Len(t, multi.Rows, 3) | |
| 36 | + | assert.Equal(t, "Smoke · push", multi.Rows[0].GroupLabel) | |
| 37 | + | assert.Equal(t, "test", multi.Rows[1].Name) | |
| 38 | + | assert.Empty(t, multi.Rows[1].GroupLabel) | |
| 39 | + | assert.Equal(t, "Smoke · pull_request", multi.Rows[2].GroupLabel) | |
| 40 | + | assert.Equal(t, runHref("proj", 8), multi.Rows[2].GroupHref) | |
| 41 | + | var html bytes.Buffer | |
| 42 | + | require.NoError(t, MRChecks(multi).Render(context.Background(), &html)) | |
| 43 | + | assert.Contains(t, html.String(), `Checks <span class="count">3</span>`) | |
| 44 | + | assert.Contains(t, html.String(), `<li class="ck-group"><a href="`+runHref("proj", 7)+`">Smoke · push</a></li>`) | |
| 45 | + | t.Log(html.String()) | |
| 46 | + | b.Event = "" | |
| 47 | + | fallback := checksSection(repo, []ports.CheckView{a, b}) | |
| 48 | + | assert.Equal(t, "Smoke · run 8", fallback.Rows[1].GroupLabel) | |
| 49 | + | } | |
| @@ -0,0 +1,49 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | package web | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "bytes" | |||
| 7 | + | "context" | |||
| 8 | + | "testing" | |||
| 9 | + | ||||
| 10 | + | "github.com/stretchr/testify/assert" | |||
| 11 | + | "github.com/stretchr/testify/require" | |||
| 12 | + | ||||
| 13 | + | "bnema.dev/ermite/internal/domain" | |||
| 14 | + | "bnema.dev/ermite/internal/ports" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | func TestChecksSectionRuns(t *testing.T) { | |||
| 18 | + | t.Parallel() | |||
| 19 | + | repo := domain.Repo{Name: repoName(t, "proj")} | |||
| 20 | + | check := func(run int64, name, event string) ports.CheckView { | |||
| 21 | + | return ports.CheckView{CICheck: domain.CICheck{RunID: run, Name: name, WorkflowName: "Smoke", Status: domain.CICompleted, Conclusion: domain.CISuccess}, Event: event} | |||
| 22 | + | } | |||
| 23 | + | a, b, c := check(7, "build", "push"), check(8, "build", "pull_request"), check(7, "test", "push") | |||
| 24 | + | single := checksSection(repo, []ports.CheckView{a, c}) | |||
| 25 | + | require.Len(t, single.Rows, 2) | |||
| 26 | + | assert.Equal(t, []CheckRow{ | |||
| 27 | + | {Name: "build", Kind: "ok", State: "success", Href: runHref("proj", 7)}, | |||
| 28 | + | {Name: "test", Kind: "ok", State: "success", Href: runHref("proj", 7)}, | |||
| 29 | + | }, single.Rows) | |||
| 30 | + | var flat bytes.Buffer | |||
| 31 | + | require.NoError(t, MRChecks(single).Render(context.Background(), &flat)) | |||
| 32 | + | assert.NotContains(t, flat.String(), "ck-group") | |||
| 33 | + | ||||
| 34 | + | multi := checksSection(repo, []ports.CheckView{a, b, c}) | |||
| 35 | + | require.Len(t, multi.Rows, 3) | |||
| 36 | + | assert.Equal(t, "Smoke · push", multi.Rows[0].GroupLabel) | |||
| 37 | + | assert.Equal(t, "test", multi.Rows[1].Name) | |||
| 38 | + | assert.Empty(t, multi.Rows[1].GroupLabel) | |||
| 39 | + | assert.Equal(t, "Smoke · pull_request", multi.Rows[2].GroupLabel) | |||
| 40 | + | assert.Equal(t, runHref("proj", 8), multi.Rows[2].GroupHref) | |||
| 41 | + | var html bytes.Buffer | |||
| 42 | + | require.NoError(t, MRChecks(multi).Render(context.Background(), &html)) | |||
| 43 | + | assert.Contains(t, html.String(), `Checks <span class="count">3</span>`) | |||
| 44 | + | assert.Contains(t, html.String(), `<li class="ck-group"><a href="`+runHref("proj", 7)+`">Smoke · push</a></li>`) | |||
| 45 | + | t.Log(html.String()) | |||
| 46 | + | b.Event = "" | |||
| 47 | + | fallback := checksSection(repo, []ports.CheckView{a, b}) | |||
| 48 | + | assert.Equal(t, "Smoke · run 8", fallback.Rows[1].GroupLabel) | |||
| 49 | + | } | |||
M internal/adapters/web/static/css/65-ci.css +3 -0
| @@ -60,6 +60,9 @@ | |||
| 60 | 60 | .attempts { padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); } | |
| 61 | 61 | .checks { display: grid; gap: var(--s1); padding: 0; margin: 0; list-style: none; font-size: var(--fs-s); } | |
| 62 | 62 | .checks li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); } | |
| 63 | + | .checks .ck-group { font-size: var(--fs-xs); color: var(--ink-dim); margin-top: var(--s2); } | |
| 64 | + | .checks .ck-group:first-child { margin-top: 0; } | |
| 65 | + | .checks .ck-group a { color: inherit; } | |
| 63 | 66 | /* the merge rule under the checks: a note about them, not one of them */ | |
| 64 | 67 | .rail section > .rule-note { | |
| 65 | 68 | display: flex; gap: 6px; margin-top: var(--s3); padding-top: var(--s2); border-top: 1px dashed var(--border); | |
| @@ -60,6 +60,9 @@ | |||||
| 60 | .attempts { padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); } | 60 | .attempts { padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); } | ||
| 61 | .checks { display: grid; gap: var(--s1); padding: 0; margin: 0; list-style: none; font-size: var(--fs-s); } | 61 | .checks { display: grid; gap: var(--s1); padding: 0; margin: 0; list-style: none; font-size: var(--fs-s); } | ||
| 62 | .checks li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); } | 62 | .checks li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1); } | ||
| 63 | + | .checks .ck-group { font-size: var(--fs-xs); color: var(--ink-dim); margin-top: var(--s2); } | |||
| 64 | + | .checks .ck-group:first-child { margin-top: 0; } | |||
| 65 | + | .checks .ck-group a { color: inherit; } | |||
| 63 | /* the merge rule under the checks: a note about them, not one of them */ | 66 | /* the merge rule under the checks: a note about them, not one of them */ | ||
| 64 | .rail section > .rule-note { | 67 | .rail section > .rule-note { | ||
| 65 | display: flex; gap: 6px; margin-top: var(--s3); padding-top: var(--s2); border-top: 1px dashed var(--border); | 68 | display: flex; gap: 6px; margin-top: var(--s3); padding-top: var(--s2); border-top: 1px dashed var(--border); | ||
M internal/ports/ci.go +2 -0
| @@ -695,6 +695,8 @@ type GetJobLogParams struct { | |||
| 695 | 695 | // CheckView is a check with what owners need to read it. | |
| 696 | 696 | type CheckView struct { | |
| 697 | 697 | domain.CICheck | |
| 698 | + | // Event is the event of the run of the check (push, pull_request, ...), empty if unknown. | |
| 699 | + | Event string | |
| 698 | 700 | // WaitingForRunner is set for a queued check whose job no registered | |
| 699 | 701 | // runner can take (no runner has all its labels). | |
| 700 | 702 | WaitingForRunner bool | |
| @@ -695,6 +695,8 @@ type GetJobLogParams struct { | |||||
| 695 | // CheckView is a check with what owners need to read it. | 695 | // CheckView is a check with what owners need to read it. | ||
| 696 | type CheckView struct { | 696 | type CheckView struct { | ||
| 697 | domain.CICheck | 697 | domain.CICheck | ||
| 698 | + | // Event is the event of the run of the check (push, pull_request, ...), empty if unknown. | |||
| 699 | + | Event string | |||
| 698 | // WaitingForRunner is set for a queued check whose job no registered | 700 | // WaitingForRunner is set for a queued check whose job no registered | ||
| 699 | // runner can take (no runner has all its labels). | 701 | // runner can take (no runner has all its labels). | ||
| 700 | WaitingForRunner bool | 702 | WaitingForRunner bool | ||
M internal/usecase/ci_runner_unit_test.go +39 -0
| @@ -421,3 +421,42 @@ func TestLogLimitEndsTheJob(t *testing.T) { | |||
| 421 | 421 | assert.Equal(t, domain.CIFailure, written.Jobs[0].Conclusion) | |
| 422 | 422 | assert.Equal(t, domain.CIStepCompleted, written.Jobs[0].Steps[0].State, "an open step is completed with the job") | |
| 423 | 423 | } | |
| 424 | + | ||
| 425 | + | func TestListChecksRunEvents(t *testing.T) { | |
| 426 | + | t.Parallel() | |
| 427 | + | for _, tc := range []struct { | |
| 428 | + | name string | |
| 429 | + | err error | |
| 430 | + | }{ | |
| 431 | + | {name: "events"}, {name: "pruned", err: domain.ErrNotFound}, {name: "store error", err: errors.New("store unavailable")}, | |
| 432 | + | } { | |
| 433 | + | t.Run(tc.name, func(t *testing.T) { | |
| 434 | + | t.Parallel() | |
| 435 | + | u := newCIUnit(t, nil) | |
| 436 | + | repo := name(t, "proj") | |
| 437 | + | sha := "0123456789012345678901234567890123456789" | |
| 438 | + | u.meta.EXPECT().Get(mock.Anything, repo).Return(domain.Repo{Name: repo, Visibility: domain.VisibilityPublic}, nil) | |
| 439 | + | u.store.EXPECT().ListChecks(mock.Anything, repo, sha, usecase.MaxCIChecks).Return([]domain.CICheck{ | |
| 440 | + | {RunID: 7, Status: domain.CICompleted}, {RunID: 8, Status: domain.CICompleted}, {RunID: 7, Status: domain.CICompleted}, | |
| 441 | + | }, false, nil).Once() | |
| 442 | + | u.store.EXPECT().GetRun(mock.Anything, repo, int64(7)).Return(domain.CIRun{Event: "push"}, tc.err).Once() | |
| 443 | + | if tc.err == nil || errors.Is(tc.err, domain.ErrNotFound) { | |
| 444 | + | u.store.EXPECT().GetRun(mock.Anything, repo, int64(8)).Return(domain.CIRun{Event: "pull_request"}, nil).Once() | |
| 445 | + | } | |
| 446 | + | res, err := u.ci.ListChecks(context.Background(), domain.Anonymous(), repo, sha) | |
| 447 | + | if tc.err != nil && !errors.Is(tc.err, domain.ErrNotFound) { | |
| 448 | + | require.ErrorIs(t, err, tc.err) | |
| 449 | + | return | |
| 450 | + | } | |
| 451 | + | require.NoError(t, err) | |
| 452 | + | require.Len(t, res.Checks, 3) | |
| 453 | + | event := "push" | |
| 454 | + | if tc.err != nil { | |
| 455 | + | event = "" | |
| 456 | + | } | |
| 457 | + | assert.Equal(t, event, res.Checks[0].Event) | |
| 458 | + | assert.Equal(t, "pull_request", res.Checks[1].Event) | |
| 459 | + | assert.Equal(t, event, res.Checks[2].Event) | |
| 460 | + | }) | |
| 461 | + | } | |
| 462 | + | } | |
| @@ -421,3 +421,42 @@ func TestLogLimitEndsTheJob(t *testing.T) { | |||||
| 421 | assert.Equal(t, domain.CIFailure, written.Jobs[0].Conclusion) | 421 | assert.Equal(t, domain.CIFailure, written.Jobs[0].Conclusion) | ||
| 422 | assert.Equal(t, domain.CIStepCompleted, written.Jobs[0].Steps[0].State, "an open step is completed with the job") | 422 | assert.Equal(t, domain.CIStepCompleted, written.Jobs[0].Steps[0].State, "an open step is completed with the job") | ||
| 423 | } | 423 | } | ||
| 424 | + | ||||
| 425 | + | func TestListChecksRunEvents(t *testing.T) { | |||
| 426 | + | t.Parallel() | |||
| 427 | + | for _, tc := range []struct { | |||
| 428 | + | name string | |||
| 429 | + | err error | |||
| 430 | + | }{ | |||
| 431 | + | {name: "events"}, {name: "pruned", err: domain.ErrNotFound}, {name: "store error", err: errors.New("store unavailable")}, | |||
| 432 | + | } { | |||
| 433 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 434 | + | t.Parallel() | |||
| 435 | + | u := newCIUnit(t, nil) | |||
| 436 | + | repo := name(t, "proj") | |||
| 437 | + | sha := "0123456789012345678901234567890123456789" | |||
| 438 | + | u.meta.EXPECT().Get(mock.Anything, repo).Return(domain.Repo{Name: repo, Visibility: domain.VisibilityPublic}, nil) | |||
| 439 | + | u.store.EXPECT().ListChecks(mock.Anything, repo, sha, usecase.MaxCIChecks).Return([]domain.CICheck{ | |||
| 440 | + | {RunID: 7, Status: domain.CICompleted}, {RunID: 8, Status: domain.CICompleted}, {RunID: 7, Status: domain.CICompleted}, | |||
| 441 | + | }, false, nil).Once() | |||
| 442 | + | u.store.EXPECT().GetRun(mock.Anything, repo, int64(7)).Return(domain.CIRun{Event: "push"}, tc.err).Once() | |||
| 443 | + | if tc.err == nil || errors.Is(tc.err, domain.ErrNotFound) { | |||
| 444 | + | u.store.EXPECT().GetRun(mock.Anything, repo, int64(8)).Return(domain.CIRun{Event: "pull_request"}, nil).Once() | |||
| 445 | + | } | |||
| 446 | + | res, err := u.ci.ListChecks(context.Background(), domain.Anonymous(), repo, sha) | |||
| 447 | + | if tc.err != nil && !errors.Is(tc.err, domain.ErrNotFound) { | |||
| 448 | + | require.ErrorIs(t, err, tc.err) | |||
| 449 | + | return | |||
| 450 | + | } | |||
| 451 | + | require.NoError(t, err) | |||
| 452 | + | require.Len(t, res.Checks, 3) | |||
| 453 | + | event := "push" | |||
| 454 | + | if tc.err != nil { | |||
| 455 | + | event = "" | |||
| 456 | + | } | |||
| 457 | + | assert.Equal(t, event, res.Checks[0].Event) | |||
| 458 | + | assert.Equal(t, "pull_request", res.Checks[1].Event) | |||
| 459 | + | assert.Equal(t, event, res.Checks[2].Event) | |||
| 460 | + | }) | |||
| 461 | + | } | |||
| 462 | + | } | |||
M internal/usecase/ci_runs.go +13 -1
| @@ -519,8 +519,20 @@ func (c *CI) ListChecks(ctx context.Context, p domain.Principal, name domain.Rep | |||
| 519 | 519 | } | |
| 520 | 520 | res := ports.ChecksResult{Truncated: more} | |
| 521 | 521 | var runners []domain.CIRunner | |
| 522 | + | events := make(map[int64]string) | |
| 522 | 523 | for _, ck := range checks { | |
| 523 | - | v := ports.CheckView{CICheck: ck} | |
| 524 | + | event, ok := events[ck.RunID] | |
| 525 | + | if !ok { | |
| 526 | + | run, err := c.cfg.Store.GetRun(ctx, name, ck.RunID) | |
| 527 | + | if err != nil && !errors.Is(err, domain.ErrNotFound) { | |
| 528 | + | return ports.ChecksResult{}, err | |
| 529 | + | } | |
| 530 | + | if err == nil { | |
| 531 | + | event = run.Event | |
| 532 | + | } | |
| 533 | + | events[ck.RunID] = event | |
| 534 | + | } | |
| 535 | + | v := ports.CheckView{CICheck: ck, Event: event} | |
| 524 | 536 | if ck.Status == domain.CIQueued { | |
| 525 | 537 | if runners == nil { | |
| 526 | 538 | runners, _ = c.cfg.Store.ListRunners(ctx) | |
| @@ -519,8 +519,20 @@ func (c *CI) ListChecks(ctx context.Context, p domain.Principal, name domain.Rep | |||||
| 519 | } | 519 | } | ||
| 520 | res := ports.ChecksResult{Truncated: more} | 520 | res := ports.ChecksResult{Truncated: more} | ||
| 521 | var runners []domain.CIRunner | 521 | var runners []domain.CIRunner | ||
| 522 | + | events := make(map[int64]string) | |||
| 522 | for _, ck := range checks { | 523 | for _, ck := range checks { | ||
| 523 | - | v := ports.CheckView{CICheck: ck} | 524 | + | event, ok := events[ck.RunID] |
| 525 | + | if !ok { | |||
| 526 | + | run, err := c.cfg.Store.GetRun(ctx, name, ck.RunID) | |||
| 527 | + | if err != nil && !errors.Is(err, domain.ErrNotFound) { | |||
| 528 | + | return ports.ChecksResult{}, err | |||
| 529 | + | } | |||
| 530 | + | if err == nil { | |||
| 531 | + | event = run.Event | |||
| 532 | + | } | |||
| 533 | + | events[ck.RunID] = event | |||
| 534 | + | } | |||
| 535 | + | v := ports.CheckView{CICheck: ck, Event: event} | |||
| 524 | if ck.Status == domain.CIQueued { | 536 | if ck.Status == domain.CIQueued { | ||
| 525 | if runners == nil { | 537 | if runners == nil { | ||
| 526 | runners, _ = c.cfg.Store.ListRunners(ctx) | 538 | runners, _ = c.cfg.Store.ListRunners(ctx) | ||