detached 4534992

feat(web): group the checks of a merge request by run when a commit has several

commit
453499267f41ae8b3e4bd97a7445ec1c9de27425
author
brice · · 2026-10-19 19:15 UTC
parent
70c00ad
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
checks
7/8
8 files · +213 -45
M internal/adapters/web/actions.templ +3 -0
@@ -456,6 +456,9 @@ templ MRChecks(c ChecksSection) {
456456 } else {
457457 <ul class="checks">
458458 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+ }
459462 <li>
460463 @CheckIcon(r.Kind, r.State)
461464 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
@@ -3146,7 +3146,39 @@ func MRChecks(c ChecksSection) templ.Component {
31463146 return templ_7745c5c3_Err
31473147 }
31483148 for _, r := range c.Rows {
3149- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "<li>")
3149+ if r.GroupLabel != "" {
3150+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "<li class=\"ck-group\"><a href=\"")
3151+ if templ_7745c5c3_Err != nil {
3152+ return templ_7745c5c3_Err
3153+ }
3154+ var templ_7745c5c3_Var165 templ.SafeURL
3155+ templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.GroupHref))
3156+ if templ_7745c5c3_Err != nil {
3157+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 460, Col: 63}
3158+ }
3159+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165))
3160+ if templ_7745c5c3_Err != nil {
3161+ return templ_7745c5c3_Err
3162+ }
3163+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "\">")
3164+ if templ_7745c5c3_Err != nil {
3165+ return templ_7745c5c3_Err
3166+ }
3167+ var templ_7745c5c3_Var166 string
3168+ templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(r.GroupLabel)
3169+ if templ_7745c5c3_Err != nil {
3170+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 460, Col: 80}
3171+ }
3172+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))
3173+ if templ_7745c5c3_Err != nil {
3174+ return templ_7745c5c3_Err
3175+ }
3176+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "</a></li>")
3177+ if templ_7745c5c3_Err != nil {
3178+ return templ_7745c5c3_Err
3179+ }
3180+ }
3181+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, " <li>")
31503182 if templ_7745c5c3_Err != nil {
31513183 return templ_7745c5c3_Err
31523184 }
@@ -3155,108 +3187,108 @@ func MRChecks(c ChecksSection) templ.Component {
31553187 return templ_7745c5c3_Err
31563188 }
31573189 if r.Href != "" {
3158- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "<a href=\"")
3190+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<a href=\"")
31593191 if templ_7745c5c3_Err != nil {
31603192 return templ_7745c5c3_Err
31613193 }
3162- var templ_7745c5c3_Var165 templ.SafeURL
3163- templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
3194+ var templ_7745c5c3_Var167 templ.SafeURL
3195+ templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
31643196 if templ_7745c5c3_Err != nil {
3165- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 462, Col: 38}
3197+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 465, Col: 38}
31663198 }
3167- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165))
3199+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))
31683200 if templ_7745c5c3_Err != nil {
31693201 return templ_7745c5c3_Err
31703202 }
3171- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "\">")
3203+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "\">")
31723204 if templ_7745c5c3_Err != nil {
31733205 return templ_7745c5c3_Err
31743206 }
3175- var templ_7745c5c3_Var166 string
3176- templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
3207+ var templ_7745c5c3_Var168 string
3208+ templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
31773209 if templ_7745c5c3_Err != nil {
3178- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 462, Col: 49}
3210+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 465, Col: 49}
31793211 }
3180- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))
3212+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168))
31813213 if templ_7745c5c3_Err != nil {
31823214 return templ_7745c5c3_Err
31833215 }
3184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "</a> ")
3216+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "</a> ")
31853217 if templ_7745c5c3_Err != nil {
31863218 return templ_7745c5c3_Err
31873219 }
31883220 } else {
3189- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<span>")
3221+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "<span>")
31903222 if templ_7745c5c3_Err != nil {
31913223 return templ_7745c5c3_Err
31923224 }
3193- var templ_7745c5c3_Var167 string
3194- templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
3225+ var templ_7745c5c3_Var169 string
3226+ templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
31953227 if templ_7745c5c3_Err != nil {
3196- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 464, Col: 21}
3228+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 467, Col: 21}
31973229 }
3198- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))
3230+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169))
31993231 if templ_7745c5c3_Err != nil {
32003232 return templ_7745c5c3_Err
32013233 }
3202- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "</span> ")
3234+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "</span> ")
32033235 if templ_7745c5c3_Err != nil {
32043236 return templ_7745c5c3_Err
32053237 }
32063238 }
3207- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "<span class=\"dim\">")
3239+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "<span class=\"dim\">")
32083240 if templ_7745c5c3_Err != nil {
32093241 return templ_7745c5c3_Err
32103242 }
3211- var templ_7745c5c3_Var168 string
3212- templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(r.State)
3243+ var templ_7745c5c3_Var170 string
3244+ templ_7745c5c3_Var170, templ_7745c5c3_Err = templ.JoinStringErrs(r.State)
32133245 if templ_7745c5c3_Err != nil {
3214- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 466, Col: 33}
3246+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 469, Col: 33}
32153247 }
3216- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168))
3248+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var170))
32173249 if templ_7745c5c3_Err != nil {
32183250 return templ_7745c5c3_Err
32193251 }
3220- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "</span> ")
3252+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "</span> ")
32213253 if templ_7745c5c3_Err != nil {
32223254 return templ_7745c5c3_Err
32233255 }
32243256 if r.Waiting {
3225- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "<span class=\"mute\">waiting for a runner</span>")
3257+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "<span class=\"mute\">waiting for a runner</span>")
32263258 if templ_7745c5c3_Err != nil {
32273259 return templ_7745c5c3_Err
32283260 }
32293261 }
3230- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "</li>")
3262+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "</li>")
32313263 if templ_7745c5c3_Err != nil {
32323264 return templ_7745c5c3_Err
32333265 }
32343266 }
3235- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "</ul>")
3267+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "</ul>")
32363268 if templ_7745c5c3_Err != nil {
32373269 return templ_7745c5c3_Err
32383270 }
32393271 }
32403272 if c.Note != "" {
3241- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "<p class=\"rule-note\"><span class=\"sr\">note: </span>")
3273+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, "<p class=\"rule-note\"><span class=\"sr\">note: </span>")
32423274 if templ_7745c5c3_Err != nil {
32433275 return templ_7745c5c3_Err
32443276 }
3245- var templ_7745c5c3_Var169 string
3246- templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(c.Note)
3277+ var templ_7745c5c3_Var171 string
3278+ templ_7745c5c3_Var171, templ_7745c5c3_Err = templ.JoinStringErrs(c.Note)
32473279 if templ_7745c5c3_Err != nil {
3248- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 475, Col: 62}
3280+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 478, Col: 62}
32493281 }
3250- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169))
3282+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var171))
32513283 if templ_7745c5c3_Err != nil {
32523284 return templ_7745c5c3_Err
32533285 }
3254- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "</p>")
3286+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "</p>")
32553287 if templ_7745c5c3_Err != nil {
32563288 return templ_7745c5c3_Err
32573289 }
32583290 }
3259- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "</section>")
3291+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, "</section>")
32603292 if templ_7745c5c3_Err != nil {
32613293 return templ_7745c5c3_Err
32623294 }
@@ -3146,7 +3146,39 @@ func MRChecks(c ChecksSection) templ.Component {
3146 return templ_7745c5c3_Err3146 return templ_7745c5c3_Err
3147 }3147 }
3148 for _, r := range c.Rows {3148 for _, r := range c.Rows {
3149- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "<li>")3149+ if r.GroupLabel != "" {
3150+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 269, "<li class=\"ck-group\"><a href=\"")
3151+ if templ_7745c5c3_Err != nil {
3152+ return templ_7745c5c3_Err
3153+ }
3154+ var templ_7745c5c3_Var165 templ.SafeURL
3155+ templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.GroupHref))
3156+ if templ_7745c5c3_Err != nil {
3157+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 460, Col: 63}
3158+ }
3159+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165))
3160+ if templ_7745c5c3_Err != nil {
3161+ return templ_7745c5c3_Err
3162+ }
3163+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "\">")
3164+ if templ_7745c5c3_Err != nil {
3165+ return templ_7745c5c3_Err
3166+ }
3167+ var templ_7745c5c3_Var166 string
3168+ templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(r.GroupLabel)
3169+ if templ_7745c5c3_Err != nil {
3170+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 460, Col: 80}
3171+ }
3172+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))
3173+ if templ_7745c5c3_Err != nil {
3174+ return templ_7745c5c3_Err
3175+ }
3176+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "</a></li>")
3177+ if templ_7745c5c3_Err != nil {
3178+ return templ_7745c5c3_Err
3179+ }
3180+ }
3181+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, " <li>")
3150 if templ_7745c5c3_Err != nil {3182 if templ_7745c5c3_Err != nil {
3151 return templ_7745c5c3_Err3183 return templ_7745c5c3_Err
3152 }3184 }
@@ -3155,108 +3187,108 @@ func MRChecks(c ChecksSection) templ.Component {
3155 return templ_7745c5c3_Err3187 return templ_7745c5c3_Err
3156 }3188 }
3157 if r.Href != "" {3189 if r.Href != "" {
3158- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 270, "<a href=\"")3190+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<a href=\"")
3159 if templ_7745c5c3_Err != nil {3191 if templ_7745c5c3_Err != nil {
3160 return templ_7745c5c3_Err3192 return templ_7745c5c3_Err
3161 }3193 }
3162- var templ_7745c5c3_Var165 templ.SafeURL3194+ var templ_7745c5c3_Var167 templ.SafeURL
3163- templ_7745c5c3_Var165, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))3195+ templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.Href))
3164 if templ_7745c5c3_Err != nil {3196 if templ_7745c5c3_Err != nil {
3165- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 462, Col: 38}3197+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 465, Col: 38}
3166 }3198 }
3167- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var165))3199+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))
3168 if templ_7745c5c3_Err != nil {3200 if templ_7745c5c3_Err != nil {
3169 return templ_7745c5c3_Err3201 return templ_7745c5c3_Err
3170 }3202 }
3171- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 271, "\">")3203+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "\">")
3172 if templ_7745c5c3_Err != nil {3204 if templ_7745c5c3_Err != nil {
3173 return templ_7745c5c3_Err3205 return templ_7745c5c3_Err
3174 }3206 }
3175- var templ_7745c5c3_Var166 string3207+ var templ_7745c5c3_Var168 string
3176- templ_7745c5c3_Var166, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)3208+ templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
3177 if templ_7745c5c3_Err != nil {3209 if templ_7745c5c3_Err != nil {
3178- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 462, Col: 49}3210+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 465, Col: 49}
3179 }3211 }
3180- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var166))3212+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168))
3181 if templ_7745c5c3_Err != nil {3213 if templ_7745c5c3_Err != nil {
3182 return templ_7745c5c3_Err3214 return templ_7745c5c3_Err
3183 }3215 }
3184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 272, "</a> ")3216+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "</a> ")
3185 if templ_7745c5c3_Err != nil {3217 if templ_7745c5c3_Err != nil {
3186 return templ_7745c5c3_Err3218 return templ_7745c5c3_Err
3187 }3219 }
3188 } else {3220 } else {
3189- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 273, "<span>")3221+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "<span>")
3190 if templ_7745c5c3_Err != nil {3222 if templ_7745c5c3_Err != nil {
3191 return templ_7745c5c3_Err3223 return templ_7745c5c3_Err
3192 }3224 }
3193- var templ_7745c5c3_Var167 string3225+ var templ_7745c5c3_Var169 string
3194- templ_7745c5c3_Var167, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)3226+ templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
3195 if templ_7745c5c3_Err != nil {3227 if templ_7745c5c3_Err != nil {
3196- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 464, Col: 21}3228+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 467, Col: 21}
3197 }3229 }
3198- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var167))3230+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169))
3199 if templ_7745c5c3_Err != nil {3231 if templ_7745c5c3_Err != nil {
3200 return templ_7745c5c3_Err3232 return templ_7745c5c3_Err
3201 }3233 }
3202- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 274, "</span> ")3234+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "</span> ")
3203 if templ_7745c5c3_Err != nil {3235 if templ_7745c5c3_Err != nil {
3204 return templ_7745c5c3_Err3236 return templ_7745c5c3_Err
3205 }3237 }
3206 }3238 }
3207- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 275, "<span class=\"dim\">")3239+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "<span class=\"dim\">")
3208 if templ_7745c5c3_Err != nil {3240 if templ_7745c5c3_Err != nil {
3209 return templ_7745c5c3_Err3241 return templ_7745c5c3_Err
3210 }3242 }
3211- var templ_7745c5c3_Var168 string3243+ var templ_7745c5c3_Var170 string
3212- templ_7745c5c3_Var168, templ_7745c5c3_Err = templ.JoinStringErrs(r.State)3244+ templ_7745c5c3_Var170, templ_7745c5c3_Err = templ.JoinStringErrs(r.State)
3213 if templ_7745c5c3_Err != nil {3245 if templ_7745c5c3_Err != nil {
3214- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 466, Col: 33}3246+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 469, Col: 33}
3215 }3247 }
3216- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var168))3248+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var170))
3217 if templ_7745c5c3_Err != nil {3249 if templ_7745c5c3_Err != nil {
3218 return templ_7745c5c3_Err3250 return templ_7745c5c3_Err
3219 }3251 }
3220- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 276, "</span> ")3252+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "</span> ")
3221 if templ_7745c5c3_Err != nil {3253 if templ_7745c5c3_Err != nil {
3222 return templ_7745c5c3_Err3254 return templ_7745c5c3_Err
3223 }3255 }
3224 if r.Waiting {3256 if r.Waiting {
3225- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 277, "<span class=\"mute\">waiting for a runner</span>")3257+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "<span class=\"mute\">waiting for a runner</span>")
3226 if templ_7745c5c3_Err != nil {3258 if templ_7745c5c3_Err != nil {
3227 return templ_7745c5c3_Err3259 return templ_7745c5c3_Err
3228 }3260 }
3229 }3261 }
3230- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 278, "</li>")3262+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "</li>")
3231 if templ_7745c5c3_Err != nil {3263 if templ_7745c5c3_Err != nil {
3232 return templ_7745c5c3_Err3264 return templ_7745c5c3_Err
3233 }3265 }
3234 }3266 }
3235- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 279, "</ul>")3267+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "</ul>")
3236 if templ_7745c5c3_Err != nil {3268 if templ_7745c5c3_Err != nil {
3237 return templ_7745c5c3_Err3269 return templ_7745c5c3_Err
3238 }3270 }
3239 }3271 }
3240 if c.Note != "" {3272 if c.Note != "" {
3241- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 280, "<p class=\"rule-note\"><span class=\"sr\">note: </span>")3273+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 283, "<p class=\"rule-note\"><span class=\"sr\">note: </span>")
3242 if templ_7745c5c3_Err != nil {3274 if templ_7745c5c3_Err != nil {
3243 return templ_7745c5c3_Err3275 return templ_7745c5c3_Err
3244 }3276 }
3245- var templ_7745c5c3_Var169 string3277+ var templ_7745c5c3_Var171 string
3246- templ_7745c5c3_Var169, templ_7745c5c3_Err = templ.JoinStringErrs(c.Note)3278+ templ_7745c5c3_Var171, templ_7745c5c3_Err = templ.JoinStringErrs(c.Note)
3247 if templ_7745c5c3_Err != nil {3279 if templ_7745c5c3_Err != nil {
3248- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 475, Col: 62}3280+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/actions.templ`, Line: 478, Col: 62}
3249 }3281 }
3250- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var169))3282+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var171))
3251 if templ_7745c5c3_Err != nil {3283 if templ_7745c5c3_Err != nil {
3252 return templ_7745c5c3_Err3284 return templ_7745c5c3_Err
3253 }3285 }
3254- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 281, "</p>")3286+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 284, "</p>")
3255 if templ_7745c5c3_Err != nil {3287 if templ_7745c5c3_Err != nil {
3256 return templ_7745c5c3_Err3288 return templ_7745c5c3_Err
3257 }3289 }
3258 }3290 }
3259- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 282, "</section>")3291+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 285, "</section>")
3260 if templ_7745c5c3_Err != nil {3292 if templ_7745c5c3_Err != nil {
3261 return templ_7745c5c3_Err3293 return templ_7745c5c3_Err
3262 }3294 }
M internal/adapters/web/checks.go +38 -10
@@ -25,6 +25,7 @@ type ChecksSummary struct {
2525 type CheckRow struct {
2626 Name, Kind, State, Href string
2727 Waiting bool
28+ GroupLabel, GroupHref string
2829 }
2930
3031 // 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
8283 // the repository that applies to them.
8384 func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection {
8485 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)
94107 }
95- sec.Rows = append(sec.Rows, row)
96108 }
97109 switch {
98110 case len(checks) == 0:
@@ -104,3 +116,19 @@ func checksSection(repo domain.Repo, checks []ports.CheckView) ChecksSection {
104116 }
105117 return sec
106118 }
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 string26 Name, Kind, State, Href string
27 Waiting bool27 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 sec117 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 @@
6060 .attempts { padding: 0; margin: var(--s2) 0 0; list-style: none; font-size: var(--fs-s); }
6161 .checks { display: grid; gap: var(--s1); padding: 0; margin: 0; list-style: none; font-size: var(--fs-s); }
6262 .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; }
6366 /* the merge rule under the checks: a note about them, not one of them */
6467 .rail section > .rule-note {
6568 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 {
695695 // CheckView is a check with what owners need to read it.
696696 type CheckView struct {
697697 domain.CICheck
698+ // Event is the event of the run of the check (push, pull_request, ...), empty if unknown.
699+ Event string
698700 // WaitingForRunner is set for a queued check whose job no registered
699701 // runner can take (no runner has all its labels).
700702 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.CICheck697 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 registered700 // 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 bool702 WaitingForRunner bool
M internal/usecase/ci_runner_unit_test.go +39 -0
@@ -421,3 +421,42 @@ func TestLogLimitEndsTheJob(t *testing.T) {
421421 assert.Equal(t, domain.CIFailure, written.Jobs[0].Conclusion)
422422 assert.Equal(t, domain.CIStepCompleted, written.Jobs[0].Steps[0].State, "an open step is completed with the job")
423423 }
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
519519 }
520520 res := ports.ChecksResult{Truncated: more}
521521 var runners []domain.CIRunner
522+ events := make(map[int64]string)
522523 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}
524536 if ck.Status == domain.CIQueued {
525537 if runners == nil {
526538 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.CIRunner521 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)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close