fix: Actions log view stops refreshing after the displayed job is finished, even if other jobs are still running (#9158)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9158 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
commit
0e25815b1f
2 changed files with 209 additions and 24 deletions
|
@ -112,29 +112,37 @@ type ViewRequest struct {
|
|||
}
|
||||
|
||||
type ViewResponse struct {
|
||||
State struct {
|
||||
Run struct {
|
||||
Link string `json:"link"`
|
||||
Title string `json:"title"`
|
||||
TitleHTML template.HTML `json:"titleHTML"`
|
||||
Status string `json:"status"`
|
||||
CanCancel bool `json:"canCancel"`
|
||||
CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
|
||||
CanRerun bool `json:"canRerun"`
|
||||
CanDeleteArtifact bool `json:"canDeleteArtifact"`
|
||||
Done bool `json:"done"`
|
||||
Jobs []*ViewJob `json:"jobs"`
|
||||
Commit ViewCommit `json:"commit"`
|
||||
} `json:"run"`
|
||||
CurrentJob struct {
|
||||
Title string `json:"title"`
|
||||
Detail string `json:"detail"`
|
||||
Steps []*ViewJobStep `json:"steps"`
|
||||
} `json:"currentJob"`
|
||||
} `json:"state"`
|
||||
Logs struct {
|
||||
StepsLog []*ViewStepLog `json:"stepsLog"`
|
||||
} `json:"logs"`
|
||||
State ViewState `json:"state"`
|
||||
Logs ViewLogs `json:"logs"`
|
||||
}
|
||||
|
||||
type ViewState struct {
|
||||
Run ViewRunInfo `json:"run"`
|
||||
CurrentJob ViewCurrentJob `json:"currentJob"`
|
||||
}
|
||||
|
||||
type ViewRunInfo struct {
|
||||
Link string `json:"link"`
|
||||
Title string `json:"title"`
|
||||
TitleHTML template.HTML `json:"titleHTML"`
|
||||
Status string `json:"status"`
|
||||
CanCancel bool `json:"canCancel"`
|
||||
CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
|
||||
CanRerun bool `json:"canRerun"`
|
||||
CanDeleteArtifact bool `json:"canDeleteArtifact"`
|
||||
Done bool `json:"done"`
|
||||
Jobs []*ViewJob `json:"jobs"`
|
||||
Commit ViewCommit `json:"commit"`
|
||||
}
|
||||
|
||||
type ViewCurrentJob struct {
|
||||
Title string `json:"title"`
|
||||
Detail string `json:"detail"`
|
||||
Steps []*ViewJobStep `json:"steps"`
|
||||
}
|
||||
|
||||
type ViewLogs struct {
|
||||
StepsLog []*ViewStepLog `json:"stepsLog"`
|
||||
}
|
||||
|
||||
type ViewJob struct {
|
||||
|
@ -211,10 +219,20 @@ func ViewPost(ctx *context_module.Context) {
|
|||
resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
|
||||
resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
|
||||
resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
|
||||
resp.State.Run.Done = run.Status.IsDone()
|
||||
resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead of 'null' in json
|
||||
resp.State.Run.Status = run.Status.String()
|
||||
|
||||
// It's possible for the run to be marked with a finalized status (eg. failure) because of a single job within the
|
||||
// run; eg. one job fails, the run fails. But other jobs can still be running. The frontend RepoActionView uses the
|
||||
// `done` flag to indicate whether to stop querying the run's status -- so even though the run has reached a final
|
||||
// state, it may not be time to stop polling for updates.
|
||||
done := run.Status.IsDone()
|
||||
|
||||
for _, v := range jobs {
|
||||
if !v.Status.IsDone() {
|
||||
// Ah, another job is still running. Keep the frontend polling enabled then.
|
||||
done = false
|
||||
}
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{
|
||||
ID: v.ID,
|
||||
Name: v.Name,
|
||||
|
@ -223,6 +241,7 @@ func ViewPost(ctx *context_module.Context) {
|
|||
Duration: v.Duration().String(),
|
||||
})
|
||||
}
|
||||
resp.State.Run.Done = done
|
||||
|
||||
pusher := ViewUser{
|
||||
DisplayName: run.TriggerUser.GetDisplayName(),
|
||||
|
|
|
@ -5,14 +5,19 @@ package actions
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
repo_model "forgejo.org/models/repo"
|
||||
unittest "forgejo.org/models/unittest"
|
||||
"forgejo.org/modules/json"
|
||||
"forgejo.org/modules/web"
|
||||
"forgejo.org/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_getRunByID(t *testing.T) {
|
||||
|
@ -135,3 +140,164 @@ func Test_artifactsFindByNameOrID(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func baseExpectedResponse() *ViewResponse {
|
||||
return &ViewResponse{
|
||||
State: ViewState{
|
||||
Run: ViewRunInfo{
|
||||
Link: "/user5/repo4/actions/runs/187",
|
||||
Title: "update actions",
|
||||
TitleHTML: template.HTML("update actions"),
|
||||
Status: "success",
|
||||
CanCancel: false,
|
||||
CanApprove: false,
|
||||
CanRerun: false,
|
||||
CanDeleteArtifact: false,
|
||||
Done: true,
|
||||
Jobs: []*ViewJob{
|
||||
{
|
||||
ID: 192,
|
||||
Name: "job_2",
|
||||
Status: "success",
|
||||
CanRerun: false,
|
||||
Duration: "1m38s",
|
||||
},
|
||||
},
|
||||
Commit: ViewCommit{
|
||||
LocaleCommit: "actions.runs.commit",
|
||||
LocalePushedBy: "actions.runs.pushed_by",
|
||||
LocaleWorkflow: "actions.runs.workflow",
|
||||
ShortSha: "c2d72f5484",
|
||||
Link: "/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
||||
Pusher: ViewUser{
|
||||
DisplayName: "user1",
|
||||
Link: "/user1",
|
||||
},
|
||||
Branch: ViewBranch{
|
||||
Name: "master",
|
||||
Link: "/user5/repo4/src/branch/master",
|
||||
IsDeleted: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
CurrentJob: ViewCurrentJob{
|
||||
Title: "job_2",
|
||||
Detail: "actions.status.success",
|
||||
Steps: []*ViewJobStep{
|
||||
{
|
||||
Summary: "Set up job",
|
||||
Status: "running",
|
||||
},
|
||||
{
|
||||
Summary: "Complete job",
|
||||
Status: "waiting",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Logs: ViewLogs{
|
||||
StepsLog: []*ViewStepLog{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsViewViewPost(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
runIndex int64
|
||||
jobIndex int64
|
||||
expected *ViewResponse
|
||||
expectedTweaks func(*ViewResponse)
|
||||
}{
|
||||
{
|
||||
name: "base case",
|
||||
runIndex: 187,
|
||||
jobIndex: 0,
|
||||
expected: baseExpectedResponse(),
|
||||
expectedTweaks: func(resp *ViewResponse) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "run with waiting jobs",
|
||||
runIndex: 189,
|
||||
jobIndex: 0,
|
||||
expected: baseExpectedResponse(),
|
||||
expectedTweaks: func(resp *ViewResponse) {
|
||||
// Variations from runIndex 187 -> runIndex 189 that are not the subject of this test...
|
||||
resp.State.Run.Link = "/user5/repo4/actions/runs/189"
|
||||
resp.State.Run.Title = "job output"
|
||||
resp.State.Run.TitleHTML = "job output"
|
||||
resp.State.Run.Jobs = []*ViewJob{
|
||||
{
|
||||
ID: 194,
|
||||
Name: "job1 (1)",
|
||||
Status: "success",
|
||||
},
|
||||
{
|
||||
ID: 195,
|
||||
Name: "job1 (2)",
|
||||
Status: "success",
|
||||
},
|
||||
{
|
||||
ID: 196,
|
||||
Name: "job2",
|
||||
Status: "waiting",
|
||||
},
|
||||
}
|
||||
resp.State.CurrentJob.Title = "job1 (1)"
|
||||
resp.State.CurrentJob.Steps = []*ViewJobStep{
|
||||
{
|
||||
Summary: "Set up job",
|
||||
Status: "success",
|
||||
},
|
||||
{
|
||||
Summary: "Complete job",
|
||||
Status: "success",
|
||||
},
|
||||
}
|
||||
|
||||
// Under test in this case: verify that Done is set to false; in the fixture data, job.ID=195 is status
|
||||
// Success, but job.ID=196 is status Waiting, and so we expect to signal Done=false to indicate to the
|
||||
// UI to continue refreshing the page.
|
||||
resp.State.Run.Done = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx, resp := contexttest.MockContext(t, "user2/repo1/actions/runs/0")
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
contexttest.LoadRepo(t, ctx, 4)
|
||||
ctx.SetParams(":run", fmt.Sprintf("%d", tt.runIndex))
|
||||
ctx.SetParams(":job", fmt.Sprintf("%d", tt.jobIndex))
|
||||
web.SetForm(ctx, &ViewRequest{})
|
||||
|
||||
ViewPost(ctx)
|
||||
require.Equal(t, http.StatusOK, resp.Result().StatusCode)
|
||||
|
||||
var actual ViewResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &actual)
|
||||
require.NoError(t, err)
|
||||
|
||||
// `Duration` field is dynamic based upon current time, so eliminate it from comparison -- but check that it
|
||||
// has the right format at least.
|
||||
zeroDurations := func(vr *ViewResponse) {
|
||||
for _, job := range vr.State.Run.Jobs {
|
||||
assert.Regexp(t, `^(\d+[hms]){1,3}$`, job.Duration)
|
||||
job.Duration = ""
|
||||
}
|
||||
for _, step := range vr.State.CurrentJob.Steps {
|
||||
step.Duration = ""
|
||||
}
|
||||
}
|
||||
zeroDurations(&actual)
|
||||
zeroDurations(tt.expected)
|
||||
tt.expectedTweaks(tt.expected)
|
||||
|
||||
assert.Equal(t, *tt.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue