Skip to content

Fix incomplete Actions status aggregations #32859

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 24 additions & 19 deletions models/actions/run_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,28 +153,33 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
}

func AggregateJobStatus(jobs []*ActionRunJob) Status {
allDone := true
allWaiting := true
hasFailure := false
allSuccessOrSkipped := true
var hasFailure, hasCancelled, hasSkipped, hasWaiting, hasRunning, hasBlocked bool
for _, job := range jobs {
if !job.Status.IsDone() {
allDone = false
}
if job.Status != StatusWaiting && !job.Status.IsDone() {
allWaiting = false
}
if job.Status == StatusFailure || job.Status == StatusCancelled {
hasFailure = true
}
allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped)
hasFailure = hasFailure || job.Status == StatusFailure
hasCancelled = hasCancelled || job.Status == StatusCancelled
hasSkipped = hasSkipped || job.Status == StatusSkipped
hasWaiting = hasWaiting || job.Status == StatusWaiting
hasRunning = hasRunning || job.Status == StatusRunning
hasBlocked = hasBlocked || job.Status == StatusBlocked
}
if allDone {
if hasFailure {
return StatusFailure
}
switch {
case allSuccessOrSkipped:
return StatusSuccess
}
if allWaiting {
case hasFailure:
return StatusFailure
case hasRunning:
return StatusRunning
case hasWaiting:
return StatusWaiting
case hasBlocked:
return StatusBlocked
case hasCancelled:
return StatusCancelled
case hasSkipped:
return StatusSkipped
default:
return StatusUnknown // it shouldn't happen
}
return StatusRunning
}
64 changes: 64 additions & 0 deletions models/actions/run_job_status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package actions

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestAggregateJobStatus(t *testing.T) {
testStatuses := func(expected Status, statuses []Status) {
var jobs []*ActionRunJob
for _, v := range statuses {
jobs = append(jobs, &ActionRunJob{Status: v})
}
actual := AggregateJobStatus(jobs)
if !assert.Equal(t, expected, actual) {
var statusStrings []string
for _, s := range statuses {
statusStrings = append(statusStrings, s.String())
}
t.Errorf("AggregateJobStatus(%v) = %v, want %v", statusStrings, statusNames[actual], statusNames[expected])
}
}

cases := []struct {
statuses []Status
expected Status
}{
// success with other status
{[]Status{StatusSuccess}, StatusSuccess},
{[]Status{StatusSuccess, StatusSkipped}, StatusSuccess}, // skipped doesn't affect success
{[]Status{StatusSuccess, StatusFailure}, StatusFailure},
{[]Status{StatusSuccess, StatusCancelled}, StatusCancelled},
{[]Status{StatusSuccess, StatusWaiting}, StatusWaiting},
{[]Status{StatusSuccess, StatusRunning}, StatusRunning},
{[]Status{StatusSuccess, StatusBlocked}, StatusBlocked},

// failure with other status, fail fast
// Should "running" win? Maybe no: old code does make "running" win, but GitHub does fail fast.
{[]Status{StatusFailure}, StatusFailure},
{[]Status{StatusFailure, StatusSuccess}, StatusFailure},
{[]Status{StatusFailure, StatusSkipped}, StatusFailure},
{[]Status{StatusFailure, StatusCancelled}, StatusFailure},
{[]Status{StatusFailure, StatusWaiting}, StatusFailure},
{[]Status{StatusFailure, StatusRunning}, StatusFailure},
{[]Status{StatusFailure, StatusBlocked}, StatusFailure},

// skipped with other status
{[]Status{StatusSkipped}, StatusSuccess},
{[]Status{StatusSkipped, StatusSuccess}, StatusSuccess},
{[]Status{StatusSkipped, StatusFailure}, StatusFailure},
{[]Status{StatusSkipped, StatusCancelled}, StatusCancelled},
{[]Status{StatusSkipped, StatusWaiting}, StatusWaiting},
{[]Status{StatusSkipped, StatusRunning}, StatusRunning},
{[]Status{StatusSkipped, StatusBlocked}, StatusBlocked},
}

for _, c := range cases {
testStatuses(c.expected, c.statuses)
}
}
18 changes: 6 additions & 12 deletions templates/repo/actions/status.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,22 @@
Please also update the vue file above if this template is modified.
action status accepted: success, skipped, waiting, blocked, running, failure, cancelled, unknown
-->
{{- $size := 16 -}}
{{- if .size -}}
{{- $size = .size -}}
{{- end -}}

{{- $className := "" -}}
{{- if .className -}}
{{- $className = .className -}}
{{- end -}}

<span class="tw-flex tw-items-center" data-tooltip-content="{{ctx.Locale.Tr (printf "actions.status.%s" .status)}}">
{{- $size := Iif .size .size 16 -}}
{{- $className := Iif .className .className "" -}}
<span data-tooltip-content="{{ctx.Locale.Tr (printf "actions.status.%s" .status)}}">
{{if eq .status "success"}}
{{svg "octicon-check-circle-fill" $size (printf "text green %s" $className)}}
{{else if eq .status "skipped"}}
{{svg "octicon-skip" $size (printf "text grey %s" $className)}}
{{else if eq .status "cancelled"}}
{{svg "octicon-stop" $size (printf "text grey %s" $className)}}
{{else if eq .status "waiting"}}
{{svg "octicon-clock" $size (printf "text yellow %s" $className)}}
{{else if eq .status "blocked"}}
{{svg "octicon-blocked" $size (printf "text yellow %s" $className)}}
{{else if eq .status "running"}}
{{svg "octicon-meter" $size (printf "text yellow job-status-rotate %s" $className)}}
{{else if or (eq .status "failure") or (eq .status "cancelled") or (eq .status "unknown")}}
{{else}}{{/*failure, unknown*/}}
{{svg "octicon-x-circle-fill" $size (printf "text red %s" $className)}}
{{end}}
</span>
13 changes: 7 additions & 6 deletions web_src/js/components/ActionRunStatus.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,25 @@
import {SvgIcon} from '../svg.ts';

withDefaults(defineProps<{
status: '',
size?: number,
className?: string,
status: 'success' | 'skipped' | 'waiting' | 'blocked' | 'running' | 'failure' | 'cancelled' | 'unknown',
size: number,
className: string,
localeStatus?: string,
}>(), {
size: 16,
className: undefined,
className: '',
localeStatus: undefined,
});
</script>

<template>
<span class="tw-flex tw-items-center" :data-tooltip-content="localeStatus" v-if="status">
<span :data-tooltip-content="localeStatus ?? status" v-if="status">
<SvgIcon name="octicon-check-circle-fill" class="text green" :size="size" :class-name="className" v-if="status === 'success'"/>
<SvgIcon name="octicon-skip" class="text grey" :size="size" :class-name="className" v-else-if="status === 'skipped'"/>
<SvgIcon name="octicon-stop" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'cancelled'"/>
<SvgIcon name="octicon-clock" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'waiting'"/>
<SvgIcon name="octicon-blocked" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'blocked'"/>
<SvgIcon name="octicon-meter" class="text yellow" :size="size" :class-name="'job-status-rotate ' + className" v-else-if="status === 'running'"/>
<SvgIcon name="octicon-x-circle-fill" class="text red" :size="size" v-else-if="['failure', 'cancelled', 'unknown'].includes(status)"/>
<SvgIcon name="octicon-x-circle-fill" class="text red" :size="size" v-else/><!-- failure, unknown -->
</span>
</template>
4 changes: 3 additions & 1 deletion web_src/js/components/RepoActionView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -551,11 +551,13 @@ export function initRepositoryActionView() {

.action-info-summary-title {
display: flex;
align-items: center;
gap: 0.5em;
}

.action-info-summary-title-text {
font-size: 20px;
margin: 0 0 0 8px;
margin: 0;
flex: 1;
overflow-wrap: anywhere;
}
Expand Down
2 changes: 2 additions & 0 deletions web_src/js/svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import octiconSidebarCollapse from '../../public/assets/img/svg/octicon-sidebar-
import octiconSidebarExpand from '../../public/assets/img/svg/octicon-sidebar-expand.svg';
import octiconSkip from '../../public/assets/img/svg/octicon-skip.svg';
import octiconStar from '../../public/assets/img/svg/octicon-star.svg';
import octiconStop from '../../public/assets/img/svg/octicon-stop.svg';
import octiconStrikethrough from '../../public/assets/img/svg/octicon-strikethrough.svg';
import octiconSync from '../../public/assets/img/svg/octicon-sync.svg';
import octiconTable from '../../public/assets/img/svg/octicon-table.svg';
Expand Down Expand Up @@ -140,6 +141,7 @@ const svgs = {
'octicon-sidebar-expand': octiconSidebarExpand,
'octicon-skip': octiconSkip,
'octicon-star': octiconStar,
'octicon-stop': octiconStop,
'octicon-strikethrough': octiconStrikethrough,
'octicon-sync': octiconSync,
'octicon-table': octiconTable,
Expand Down
Loading