chore: initialize Gitea v1.27.2 fork
giteabot backport / giteabot (push) Canceled after 0s
giteabot / giteabot (push) Canceled after 0s
release-nightly / nightly-binary (push) Canceled after 0s
release-nightly / nightly-container (push) Canceled after 0s
cache-seeder / gobuild (push) Canceled after 0s
cache-seeder / lint (bindata, lint-backend) (push) Canceled after 0s
release-nightly-snapcraft / build-and-publish (push) Canceled after 0s
giteabot backport / giteabot (push) Canceled after 0s
giteabot / giteabot (push) Canceled after 0s
release-nightly / nightly-binary (push) Canceled after 0s
release-nightly / nightly-container (push) Canceled after 0s
cache-seeder / gobuild (push) Canceled after 0s
cache-seeder / lint (bindata, lint-backend) (push) Canceled after 0s
release-nightly-snapcraft / build-and-publish (push) Canceled after 0s
Includes direct password setup links in registration emails. Assisted-by: Codex:GPT-5
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package devtest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"gitea.dev/models/asymkey"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/gituser"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/badge"
|
||||
"gitea.dev/modules/charset"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/indexer/code"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// List all devtest templates, they will be used for e2e tests for the UI components
|
||||
func List(ctx *context.Context) {
|
||||
templateNames, err := templates.AssetFS().ListFiles("devtest", true)
|
||||
if err != nil {
|
||||
ctx.ServerError("AssetFS().ListFiles", err)
|
||||
return
|
||||
}
|
||||
var subNames []string
|
||||
for _, tmplName := range templateNames {
|
||||
subName := strings.TrimSuffix(tmplName, ".tmpl")
|
||||
if !strings.HasPrefix(subName, "devtest-") {
|
||||
subNames = append(subNames, subName)
|
||||
}
|
||||
}
|
||||
ctx.Data["SubNames"] = subNames
|
||||
ctx.HTML(http.StatusOK, "devtest/devtest-list")
|
||||
}
|
||||
|
||||
func FetchActionTest(ctx *context.Context) {
|
||||
_ = ctx.Req.ParseForm()
|
||||
ctx.Flash.Info("fetch action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "\n" +
|
||||
"Form: " + ctx.Req.Form.Encode() + "\n" +
|
||||
"PostForm: " + ctx.Req.PostForm.Encode(),
|
||||
)
|
||||
time.Sleep(2 * time.Second)
|
||||
ctx.JSONRedirect("")
|
||||
}
|
||||
|
||||
func prepareMockDataGiteaUI(_ *context.Context) {}
|
||||
|
||||
func prepareMockDataBadgeCommitSign(ctx *context.Context) {
|
||||
var commits []*asymkey.SignCommit
|
||||
mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 1}})
|
||||
mockUser := mockUsers[0]
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
Verification: &asymkey.CommitVerification{},
|
||||
UserCommit: &gituser.UserCommit{
|
||||
GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
},
|
||||
})
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
Verification: &asymkey.CommitVerification{
|
||||
Verified: true,
|
||||
Reason: "name / key-id",
|
||||
SigningUser: mockUser,
|
||||
SigningKey: &asymkey.GPGKey{KeyID: "12345678"},
|
||||
TrustStatus: "trusted",
|
||||
},
|
||||
UserCommit: &gituser.UserCommit{
|
||||
AuthorUser: mockUser,
|
||||
GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
},
|
||||
})
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
Verification: &asymkey.CommitVerification{
|
||||
Verified: true,
|
||||
Reason: "name / key-id",
|
||||
SigningUser: mockUser,
|
||||
SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
|
||||
TrustStatus: "untrusted",
|
||||
},
|
||||
UserCommit: &gituser.UserCommit{
|
||||
AuthorUser: mockUser,
|
||||
GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
},
|
||||
})
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
Verification: &asymkey.CommitVerification{
|
||||
Verified: true,
|
||||
Reason: "name / key-id",
|
||||
SigningUser: mockUser,
|
||||
SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
|
||||
TrustStatus: "other(unmatch)",
|
||||
},
|
||||
UserCommit: &gituser.UserCommit{
|
||||
AuthorUser: mockUser,
|
||||
GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
},
|
||||
})
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
Verification: &asymkey.CommitVerification{
|
||||
Warning: true,
|
||||
Reason: "gpg.error",
|
||||
SigningEmail: "test@example.com",
|
||||
},
|
||||
UserCommit: &gituser.UserCommit{
|
||||
AuthorUser: mockUser,
|
||||
GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
},
|
||||
})
|
||||
|
||||
ctx.Data["MockCommits"] = commits
|
||||
}
|
||||
|
||||
func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
|
||||
fontFamilyNames := strings.Split(badge.DefaultFontFamily, ",")
|
||||
selectedFontFamilyName := ctx.FormString("font", fontFamilyNames[0])
|
||||
selectedStyle := ctx.FormString("style", badge.DefaultStyle)
|
||||
var badges []badge.Badge
|
||||
badges = append(badges, badge.GenerateBadge("啊啊啊啊啊啊啊啊啊啊啊啊", "🌞🌞🌞🌞🌞", "green"))
|
||||
for r := range rune(256) {
|
||||
if unicode.IsPrint(r) {
|
||||
s := strings.Repeat(string(r), 15)
|
||||
badges = append(badges, badge.GenerateBadge(s, util.TruncateRunes(s, 7), "green"))
|
||||
}
|
||||
}
|
||||
|
||||
var badgeSVGs []template.HTML
|
||||
for i, b := range badges {
|
||||
b.IDPrefix = "devtest-" + strconv.FormatInt(int64(i), 10) + "-"
|
||||
b.FontFamily = selectedFontFamilyName
|
||||
var h template.HTML
|
||||
var err error
|
||||
switch selectedStyle {
|
||||
case badge.StyleFlat:
|
||||
h, err = ctx.RenderToHTML("shared/actions/runner_badge_flat", map[string]any{"Badge": b})
|
||||
case badge.StyleFlatSquare:
|
||||
h, err = ctx.RenderToHTML("shared/actions/runner_badge_flat-square", map[string]any{"Badge": b})
|
||||
default:
|
||||
err = fmt.Errorf("unknown badge style: %s", selectedStyle)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.ServerError("RenderToHTML", err)
|
||||
return
|
||||
}
|
||||
badgeSVGs = append(badgeSVGs, h)
|
||||
}
|
||||
ctx.Data["BadgeSVGs"] = badgeSVGs
|
||||
ctx.Data["BadgeFontFamilyNames"] = fontFamilyNames
|
||||
ctx.Data["SelectedFontFamilyName"] = selectedFontFamilyName
|
||||
ctx.Data["BadgeStyles"] = badge.GlobalVars().AllStyles
|
||||
ctx.Data["SelectedStyle"] = selectedStyle
|
||||
}
|
||||
|
||||
func prepareMockDataAvatarStack(ctx *context.Context) {
|
||||
/*
|
||||
mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 3}})
|
||||
if len(mockUsers) == 0 {
|
||||
return
|
||||
}
|
||||
u0 := mockUsers[0]
|
||||
u1, u2 := u0, u0
|
||||
if len(mockUsers) >= 2 {
|
||||
u1 = mockUsers[1]
|
||||
}
|
||||
if len(mockUsers) >= 3 {
|
||||
u2 = mockUsers[2]
|
||||
}
|
||||
|
||||
authorSig := func(u *user_model.User) *git.Signature {
|
||||
return &git.Signature{Name: u.Name, Email: u.Email}
|
||||
}
|
||||
coLinked := func(u *user_model.User) *gituser.CommitParticipant {
|
||||
return &gituser.CommitParticipant{GiteaUser: u, GitIdentity: authorSig(u)}
|
||||
}
|
||||
coUnlinked := func(name, email string) *gituser.CommitParticipant {
|
||||
return &gituser.CommitParticipant{GitIdentity: &git.Signature{Name: name, Email: email}}
|
||||
}
|
||||
nUnlinked := func(n int) []*gituser.CommitParticipant {
|
||||
out := make([]*gituser.CommitParticipant, n)
|
||||
for i := range out {
|
||||
out[i] = coUnlinked(fmt.Sprintf("Contributor %d", i+1), fmt.Sprintf("contrib%d@example.com", i+1))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type scenario struct {
|
||||
Label string
|
||||
Data *gituser.AvatarStackData
|
||||
}
|
||||
mk := gituser.BuildAvatarStackData()
|
||||
extSig := &git.Signature{Name: "External Contributor", Email: "external@example.com"}
|
||||
ctx.Data["AvatarStackScenarios"] = []scenario{
|
||||
{Label: "linked author, no co-authors", Data: mk(u0, authorSig(u0), nil)},
|
||||
{Label: "unlinked author, no co-authors", Data: mk(nil, extSig, nil)},
|
||||
{Label: "1 linked co-author", Data: mk(u0, authorSig(u0), []*gituser.CommitParticipant{coLinked(u1)})},
|
||||
{Label: "1 unlinked co-author", Data: mk(u0, authorSig(u0), []*gituser.CommitParticipant{coUnlinked("Bob Smith", "bob@example.com")})},
|
||||
{Label: "2 co-authors (3 people), u1 author", Data: mk(u1, authorSig(u1), []*gituser.CommitParticipant{coLinked(u0), coUnlinked("Bob Smith", "bob@example.com")})},
|
||||
{Label: "3 co-authors mixed (4 people)", Data: mk(u0, authorSig(u0), []*gituser.CommitParticipant{coLinked(u1), coLinked(u2), coUnlinked("Bob Smith", "bob@example.com")})},
|
||||
{Label: "9 co-authors (max visible, no overflow), u2 author", Data: mk(u2, authorSig(u2), nUnlinked(9))},
|
||||
{Label: "10 co-authors (overflow +1)", Data: mk(u0, authorSig(u0), nUnlinked(10))},
|
||||
{Label: "15 co-authors (overflow +6), unlinked author", Data: mk(nil, extSig, nUnlinked(15))},
|
||||
{Label: "30 co-authors (overflow +21)", Data: mk(u0, authorSig(u0), nUnlinked(30))},
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
func prepareMockDataRelativeTime(ctx *context.Context) {
|
||||
now := time.Now()
|
||||
ctx.Data["TimeNow"] = now
|
||||
ctx.Data["TimePast5s"] = now.Add(-5 * time.Second)
|
||||
ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second)
|
||||
ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute)
|
||||
ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute)
|
||||
ctx.Data["TimePast3m"] = now.Add(-3 * time.Minute)
|
||||
ctx.Data["TimePast1h"] = now.Add(-1 * time.Hour)
|
||||
ctx.Data["TimePast3h"] = now.Add(-3 * time.Hour)
|
||||
ctx.Data["TimePast1d"] = now.Add(-24 * time.Hour)
|
||||
ctx.Data["TimePast2d"] = now.Add(-2 * 24 * time.Hour)
|
||||
ctx.Data["TimePast3d"] = now.Add(-3 * 24 * time.Hour)
|
||||
ctx.Data["TimePast26h"] = now.Add(-26 * time.Hour)
|
||||
ctx.Data["TimePast40d"] = now.Add(-40 * 24 * time.Hour)
|
||||
ctx.Data["TimePast60d"] = now.Add(-60 * 24 * time.Hour)
|
||||
ctx.Data["TimePast1y"] = now.Add(-366 * 24 * time.Hour)
|
||||
ctx.Data["TimeFuture1h"] = now.Add(1 * time.Hour)
|
||||
ctx.Data["TimeFuture3h"] = now.Add(3 * time.Hour)
|
||||
ctx.Data["TimeFuture3d"] = now.Add(3 * 24 * time.Hour)
|
||||
ctx.Data["TimeFuture1y"] = now.Add(366 * 24 * time.Hour)
|
||||
}
|
||||
|
||||
func prepareMockData(ctx *context.Context) {
|
||||
switch ctx.Req.URL.Path {
|
||||
case "/devtest/gitea-ui":
|
||||
prepareMockDataGiteaUI(ctx)
|
||||
case "/devtest/badge-commit-sign":
|
||||
prepareMockDataBadgeCommitSign(ctx)
|
||||
case "/devtest/badge-actions-svg":
|
||||
prepareMockDataBadgeActionsSvg(ctx)
|
||||
case "/devtest/relative-time":
|
||||
prepareMockDataRelativeTime(ctx)
|
||||
case "/devtest/toast-and-message":
|
||||
prepareMockDataToastAndMessage(ctx)
|
||||
case "/devtest/unicode-escape":
|
||||
prepareMockDataUnicodeEscape(ctx)
|
||||
case "/devtest/avatar-stack":
|
||||
prepareMockDataAvatarStack(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func prepareMockDataToastAndMessage(ctx *context.Context) {
|
||||
msgWithDetails, _ := ctx.RenderToHTML("base/alert_details", map[string]any{
|
||||
"Message": "message with details <script>escape xss</script>",
|
||||
"Summary": "summary with details",
|
||||
"Details": "details line 1\n details line 2\n details line 3",
|
||||
})
|
||||
msgWithSummary, _ := ctx.RenderToHTML("base/alert_details", map[string]any{
|
||||
"Message": "message with summary <script>escape xss</script>",
|
||||
"Summary": "summary only",
|
||||
})
|
||||
|
||||
ctx.Flash.ErrorMsg = string(msgWithDetails)
|
||||
ctx.Flash.WarningMsg = string(msgWithSummary)
|
||||
ctx.Flash.InfoMsg = "a long message with line break\nthe second line <script>removed xss</script>"
|
||||
ctx.Flash.SuccessMsg = "single line message <script>removed xss</script>"
|
||||
ctx.Data["Flash"] = ctx.Flash
|
||||
}
|
||||
|
||||
func prepareMockDataUnicodeEscape(ctx *context.Context) {
|
||||
content := "// demo code\n"
|
||||
content += "if accessLevel != \"user\u202E \u2066// Check if admin (invisible char)\u2069 \u2066\" { }\n"
|
||||
content += "if O𝐾 { } // ambiguous char\n"
|
||||
content += "if O𝐾 && accessLevel != \"user\u202E \u2066// ambiguous char + invisible char\u2069 \u2066\" { }\n"
|
||||
content += "str := `\xef` // broken char\n"
|
||||
content += "str := `\x00 \x19 \x7f` // control char\n"
|
||||
|
||||
lineNums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
|
||||
highlightLines := code.HighlightSearchResultCode("demo.go", "", lineNums, content)
|
||||
escapeStatus := &charset.EscapeStatus{}
|
||||
lineEscapeStatus := make([]*charset.EscapeStatus, len(highlightLines))
|
||||
for i, hl := range highlightLines {
|
||||
lineEscapeStatus[i], hl.FormattedContent = charset.EscapeControlHTML(hl.FormattedContent, ctx.Locale)
|
||||
escapeStatus = escapeStatus.Or(lineEscapeStatus[i])
|
||||
}
|
||||
ctx.Data["HighlightLines"] = highlightLines
|
||||
ctx.Data["EscapeStatus"] = escapeStatus
|
||||
ctx.Data["LineEscapeStatus"] = lineEscapeStatus
|
||||
}
|
||||
|
||||
func TmplCommon(ctx *context.Context) {
|
||||
prepareMockData(ctx)
|
||||
if ctx.Req.Method == http.MethodPost && ctx.FormBool("mock_response_delay") {
|
||||
ctx.Flash.Info("form submit: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"\n"+
|
||||
"Form: "+ctx.Req.Form.Encode()+"\n"+
|
||||
"PostForm: "+ctx.Req.PostForm.Encode(),
|
||||
true,
|
||||
)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
ctx.HTML(http.StatusOK, templates.TplName("devtest"+path.Clean("/"+ctx.PathParam("sub"))))
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package devtest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/mailer"
|
||||
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func MailPreviewRender(ctx *context.Context) {
|
||||
tmplName := ctx.PathParam("*")
|
||||
mockDataContent, err := templates.AssetFS().ReadFile("mail/" + tmplName + ".devtest.yml")
|
||||
mockData := map[string]any{}
|
||||
if err == nil {
|
||||
err = yaml.Unmarshal(mockDataContent, &mockData)
|
||||
if err != nil {
|
||||
http.Error(ctx.Resp, "Failed to parse mock data: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
mockData["locale"] = ctx.Locale
|
||||
err = mailer.LoadedTemplates().BodyTemplates.ExecuteTemplate(ctx.Resp, tmplName, mockData)
|
||||
if err != nil {
|
||||
_, _ = ctx.Resp.Write([]byte(err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func prepareMailPreviewRender(ctx *context.Context, tmplName string) {
|
||||
tmplSubject := mailer.LoadedTemplates().SubjectTemplates.Lookup(tmplName)
|
||||
// FIXME: MAIL-TEMPLATE-SUBJECT: only "issue" related messages support using subject from templates
|
||||
subject := "(default subject)"
|
||||
if tmplSubject != nil {
|
||||
var buf strings.Builder
|
||||
err := tmplSubject.Execute(&buf, nil)
|
||||
if err != nil {
|
||||
subject = "ERROR: " + err.Error()
|
||||
} else {
|
||||
subject = util.IfZero(buf.String(), subject)
|
||||
}
|
||||
}
|
||||
ctx.Data["RenderMailSubject"] = subject
|
||||
ctx.Data["RenderMailTemplateName"] = tmplName
|
||||
}
|
||||
|
||||
func MailPreview(ctx *context.Context) {
|
||||
ctx.Data["MailTemplateNames"] = mailer.LoadedTemplates().TemplateNames
|
||||
tmplName := ctx.FormString("tmpl")
|
||||
if tmplName != "" {
|
||||
prepareMailPreviewRender(ctx, tmplName)
|
||||
}
|
||||
ctx.HTML(http.StatusOK, "devtest/mail-preview")
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package devtest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
mathRand "math/rand/v2"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/web/repo/actions"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
type generateMockStepsLogOptions struct {
|
||||
mockCountFirst int
|
||||
mockCountGeneral int
|
||||
groupRepeat int
|
||||
}
|
||||
|
||||
func generateMockStepsLog(logCur actions.LogCursor, opts generateMockStepsLogOptions) (stepsLog []*actions.ViewStepLog) {
|
||||
var mockedLogs []string
|
||||
mockedLogs = append(mockedLogs, "::group::test group for: step={step}, cursor={cursor}")
|
||||
mockedLogs = append(mockedLogs, slices.Repeat([]string{"in group msg for: step={step}, cursor={cursor}"}, opts.groupRepeat)...)
|
||||
mockedLogs = append(mockedLogs, "::endgroup::")
|
||||
mockedLogs = append(mockedLogs,
|
||||
"message for: step={step}, cursor={cursor}",
|
||||
"message for: step={step}, cursor={cursor}",
|
||||
"##[group]test group for: step={step}, cursor={cursor}",
|
||||
"in group msg for: step={step}, cursor={cursor}",
|
||||
"##[endgroup]",
|
||||
"::error::mock error for: step={step}, cursor={cursor}",
|
||||
"::warning::mock warning for: step={step}, cursor={cursor}",
|
||||
"::notice::mock notice for: step={step}, cursor={cursor}",
|
||||
"::debug::mock debug for: step={step}, cursor={cursor}",
|
||||
)
|
||||
// usually the cursor is the "file offset", but here we abuse it as "line number" to make the mock easier, intentionally
|
||||
cur := logCur.Cursor
|
||||
// for the first batch, return as many as possible to test the auto-expand and auto-scroll
|
||||
mockCount := util.Iif(logCur.Cursor == 0, opts.mockCountFirst, opts.mockCountGeneral)
|
||||
for range mockCount {
|
||||
logStr := mockedLogs[int(cur)%len(mockedLogs)]
|
||||
cur++
|
||||
logStr = strings.ReplaceAll(logStr, "{step}", strconv.Itoa(logCur.Step))
|
||||
logStr = strings.ReplaceAll(logStr, "{cursor}", strconv.FormatInt(cur, 10))
|
||||
stepsLog = append(stepsLog, &actions.ViewStepLog{
|
||||
Step: logCur.Step,
|
||||
Cursor: cur,
|
||||
Started: time.Now().Unix() - 1,
|
||||
Lines: []*actions.ViewStepLogLine{
|
||||
{Index: cur, Message: logStr, Timestamp: float64(time.Now().UnixNano()) / float64(time.Second)},
|
||||
},
|
||||
})
|
||||
}
|
||||
return stepsLog
|
||||
}
|
||||
|
||||
func MockActionsView(ctx *context.Context) {
|
||||
if runID := ctx.PathParamInt64("run"); runID == 0 {
|
||||
ctx.Redirect("/repo-action-view/runs/10")
|
||||
return
|
||||
}
|
||||
ctx.Data["JobID"] = ctx.PathParamInt64("job")
|
||||
ctx.Data["ActionsViewURL"] = ctx.Req.URL.Path
|
||||
ctx.HTML(http.StatusOK, "devtest/repo-action-view")
|
||||
}
|
||||
|
||||
func MockActionsRunsJobs(ctx *context.Context) {
|
||||
runID := ctx.PathParamInt64("run")
|
||||
attemptID := ctx.PathParamInt64("attempt")
|
||||
|
||||
alignTime := func(v, unit int64) int64 {
|
||||
return (v + unit) / unit * unit
|
||||
}
|
||||
resp := &actions.ViewResponse{}
|
||||
resp.State.Run.RepoID = 12345
|
||||
resp.State.Run.Index = runID
|
||||
resp.State.Run.TitleHTML = `mock run title <a href="/">link</a>`
|
||||
resp.State.Run.Link = setting.AppSubURL + "/devtest/repo-action-view/runs/" + strconv.FormatInt(runID, 10)
|
||||
resp.State.Run.CanDeleteArtifact = true
|
||||
resp.State.Run.WorkflowID = "workflow-id.yml"
|
||||
resp.State.Run.TriggerEvent = "push"
|
||||
renderUtils := templates.NewRenderUtils(ctx)
|
||||
user2, _ := user_model.GetUserByID(ctx, 2)
|
||||
if user2 == nil {
|
||||
user2 = &user_model.User{Name: "user2"}
|
||||
}
|
||||
user3, _ := user_model.GetUserByID(ctx, 3)
|
||||
if user3 == nil {
|
||||
user3 = &user_model.User{Name: "user3"}
|
||||
}
|
||||
resp.State.Run.Commit = actions.ViewCommit{
|
||||
ShortSha: "ccccdddd",
|
||||
Link: "./commit-link",
|
||||
Pusher: actions.ViewUser{
|
||||
DisplayName: user2.GetDisplayName(),
|
||||
Link: user2.HomeLink(),
|
||||
AvatarLink: user2.AvatarLinkWithSize(ctx, 16),
|
||||
},
|
||||
Branch: actions.ViewBranch{
|
||||
Name: "user2:commit-branch",
|
||||
Link: "./branch-link",
|
||||
IsDeleted: false,
|
||||
},
|
||||
}
|
||||
resp.State.Run.PullRequest = &actions.ViewPullRequest{
|
||||
Index: "#37658",
|
||||
Link: "./pull/37658",
|
||||
}
|
||||
now := time.Now()
|
||||
currentAttemptNum := int64(1)
|
||||
if attemptID > 0 {
|
||||
currentAttemptNum = attemptID
|
||||
}
|
||||
attempts := []*actions_model.ActionRunAttempt{{
|
||||
Attempt: 1,
|
||||
Status: actions_model.StatusSuccess,
|
||||
Created: timeutil.TimeStamp(now.Add(-time.Hour).Unix()),
|
||||
TriggerUserID: 2,
|
||||
TriggerUser: user2,
|
||||
}}
|
||||
if runID == 10 {
|
||||
attempts = []*actions_model.ActionRunAttempt{
|
||||
{
|
||||
Attempt: 3,
|
||||
Status: actions_model.StatusSuccess,
|
||||
Created: timeutil.TimeStamp(alignTime(now.Add(-time.Hour).Unix(), 3600)),
|
||||
TriggerUserID: 2,
|
||||
TriggerUser: user2,
|
||||
},
|
||||
{
|
||||
Attempt: 2,
|
||||
Status: actions_model.StatusFailure,
|
||||
Created: timeutil.TimeStamp(alignTime(now.Add(-2*time.Hour).Unix(), 3600)),
|
||||
TriggerUserID: 1,
|
||||
TriggerUser: user3,
|
||||
},
|
||||
{
|
||||
Attempt: 1,
|
||||
Status: actions_model.StatusSuccess,
|
||||
Created: timeutil.TimeStamp(alignTime(now.Add(-3*time.Hour).Unix(), 3600)),
|
||||
TriggerUserID: 2,
|
||||
TriggerUser: user2,
|
||||
},
|
||||
}
|
||||
if attemptID == 0 {
|
||||
currentAttemptNum = 3
|
||||
}
|
||||
}
|
||||
|
||||
latestAttempt := attempts[0]
|
||||
resp.State.Run.RunAttempt = currentAttemptNum
|
||||
resp.State.Run.Done = latestAttempt.Status.IsDone()
|
||||
resp.State.Run.Status = latestAttempt.Status.String()
|
||||
resp.State.Run.Duration = "1h 23m 45s"
|
||||
resp.State.Run.TriggeredAt = latestAttempt.Created.AsTime().Unix()
|
||||
resp.State.Run.ViewLink = resp.State.Run.Link
|
||||
for _, attempt := range attempts {
|
||||
link := resp.State.Run.Link
|
||||
if attempt.Attempt != latestAttempt.Attempt {
|
||||
link = fmt.Sprintf("%s/attempts/%d", resp.State.Run.Link, attempt.Attempt)
|
||||
}
|
||||
current := attempt.Attempt == currentAttemptNum
|
||||
if current {
|
||||
resp.State.Run.Status = attempt.Status.String()
|
||||
resp.State.Run.Done = attempt.Status.IsDone()
|
||||
resp.State.Run.TriggeredAt = attempt.Created.AsTime().Unix()
|
||||
if attempt.Attempt != latestAttempt.Attempt {
|
||||
resp.State.Run.ViewLink = link
|
||||
}
|
||||
}
|
||||
resp.State.Run.Attempts = append(resp.State.Run.Attempts, &actions.ViewRunAttempt{
|
||||
Attempt: attempt.Attempt,
|
||||
Status: attempt.Status.String(),
|
||||
Done: attempt.Status.IsDone(),
|
||||
Link: link,
|
||||
Current: current,
|
||||
Latest: attempt.Attempt == latestAttempt.Attempt,
|
||||
TriggeredAt: attempt.Created.AsTime().Unix(),
|
||||
TriggerUserName: attempt.TriggerUser.GetDisplayName(),
|
||||
TriggerUserLink: attempt.TriggerUser.HomeLink(),
|
||||
TriggerUserAvatar: attempt.TriggerUser.AvatarLinkWithSize(ctx, 16),
|
||||
})
|
||||
}
|
||||
isLatestAttempt := currentAttemptNum == latestAttempt.Attempt
|
||||
resp.State.Run.CanCancel = runID == 10 && isLatestAttempt
|
||||
resp.State.Run.CanApprove = runID == 20 && isLatestAttempt
|
||||
resp.State.Run.CanRerun = runID == 30 && isLatestAttempt
|
||||
resp.State.Run.CanRerunFailed = runID == 30 && isLatestAttempt
|
||||
|
||||
// Mock job summaries so the devtest page can preview the Summary panel rendering.
|
||||
// Only some runs have summaries, so the page also exercises the "no summary" state.
|
||||
if runID == 10 || runID == 20 {
|
||||
resp.State.Run.JobSummaries = []*actions.ViewJobSummary{
|
||||
{
|
||||
JobID: runID * 10,
|
||||
JobName: "job 100 (testsubname)",
|
||||
SummaryHTML: renderUtils.MarkdownToHtml("### Devtest job summary\n\n- Markdown rendering\n- Links: [example](https://example.com)\n\n```sh\necho hello\n```\n"),
|
||||
},
|
||||
{
|
||||
JobID: runID*10 + 2,
|
||||
JobName: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit",
|
||||
SummaryHTML: renderUtils.MarkdownToHtml("### Another summary\n\nThis demonstrates multiple job summaries in one run.\n\n- Item A\n- Item B\n"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
|
||||
Name: "artifact-a",
|
||||
Size: 100 * 1024,
|
||||
Status: "expired",
|
||||
ExpiresUnix: alignTime(time.Now().Add(-24*time.Hour).Unix(), 3600),
|
||||
})
|
||||
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
|
||||
Name: "artifact-b",
|
||||
Size: 1024 * 1024,
|
||||
Status: "completed",
|
||||
ExpiresUnix: alignTime(time.Now().Add(24*time.Hour).Unix(), 3600),
|
||||
})
|
||||
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
|
||||
Name: "artifact-very-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
|
||||
Size: 100 * 1024,
|
||||
Status: "expired",
|
||||
ExpiresUnix: alignTime(time.Now().Add(-24*time.Hour).Unix(), 3600),
|
||||
})
|
||||
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
|
||||
Name: "artifact-really-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
|
||||
Size: 1024 * 1024,
|
||||
Status: "completed",
|
||||
ExpiresUnix: 0,
|
||||
})
|
||||
|
||||
jobLink := func(jobID int64) string {
|
||||
return fmt.Sprintf("%s/jobs/%d", resp.State.Run.Link, jobID)
|
||||
}
|
||||
|
||||
// Keep devtest mock runs minimal: use run 10 as a "complex graph" repro.
|
||||
// This combines long durations, parallel roots, and a multi-dependency downstream job
|
||||
// to validate the workflow graph rendering.
|
||||
if runID == 10 {
|
||||
resp.State.Run.WorkflowID = "workflow-devtest-complex"
|
||||
resp.State.Run.Duration = "7h 12m 34s"
|
||||
|
||||
type mj struct {
|
||||
jobID string
|
||||
name string
|
||||
status actions_model.Status
|
||||
duration string
|
||||
needs []string
|
||||
}
|
||||
mockJobs := []mj{
|
||||
{jobID: "job-100", name: "job-100", status: actions_model.StatusSuccess, duration: "3s", needs: nil},
|
||||
{jobID: "job-101", name: "job-101", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"job-100"}},
|
||||
{jobID: "job-102", name: "job-102", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"job-100", "job-101"}},
|
||||
{jobID: "job-103", name: "job-103", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"job-100"}},
|
||||
|
||||
{jobID: "prep-jdk", name: "prep-jdk", status: actions_model.StatusSuccess, duration: "3s", needs: nil},
|
||||
{jobID: "code-analysis", name: "code-analysis", status: actions_model.StatusSuccess, duration: "3s", needs: nil},
|
||||
|
||||
// Matrix expansion (the " (...)" suffix is the heuristic the frontend uses to group rows)
|
||||
{jobID: "matrix-e2e-1-chromium", name: "matrix-e2e (1, chromium)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e-1-firefox", name: "matrix-e2e (1, firefox)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e-2-chromium", name: "matrix-e2e (2, chromium)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e-3-chromium", name: "matrix-e2e (3, chromium)", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e-3-firefox", name: "matrix-e2e (3, firefox)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e-99-webkit", name: "matrix-e2e (99, webkit)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
|
||||
{jobID: "unit-test", name: "unit-test", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "arch-test", name: "arch-test", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "integration-test", name: "integration-test", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"prep-jdk"}},
|
||||
|
||||
{jobID: "build-image", name: "build-image", status: actions_model.StatusSuccess, duration: "3s", needs: []string{
|
||||
"unit-test",
|
||||
"arch-test",
|
||||
"integration-test",
|
||||
"code-analysis",
|
||||
"matrix-e2e-1-chromium",
|
||||
"matrix-e2e-1-firefox",
|
||||
"matrix-e2e-2-chromium",
|
||||
"matrix-e2e-3-chromium",
|
||||
"matrix-e2e-3-firefox",
|
||||
"matrix-e2e-99-webkit",
|
||||
}},
|
||||
}
|
||||
|
||||
resp.State.Run.Jobs = nil
|
||||
for i, j := range mockJobs {
|
||||
id := runID*1000 + int64(i)
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: id,
|
||||
Link: jobLink(id),
|
||||
JobID: j.jobID,
|
||||
Name: j.name,
|
||||
Status: j.status.String(),
|
||||
CanRerun: j.jobID == "job-100",
|
||||
Duration: j.duration,
|
||||
Needs: j.needs,
|
||||
})
|
||||
}
|
||||
|
||||
fillViewRunResponseCurrentJob(ctx, resp)
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: runID * 10,
|
||||
Link: jobLink(runID * 10),
|
||||
JobID: "job-100",
|
||||
Name: "job 100 (testsubname)",
|
||||
Status: actions_model.StatusRunning.String(),
|
||||
CanRerun: true,
|
||||
Duration: "1h23m45s",
|
||||
})
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: runID*10 + 1,
|
||||
Link: jobLink(runID*10 + 1),
|
||||
JobID: "job-101",
|
||||
Name: "job 101",
|
||||
Status: actions_model.StatusWaiting.String(),
|
||||
CanRerun: false,
|
||||
Duration: "2h",
|
||||
Needs: []string{"job-100"},
|
||||
})
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: runID*10 + 2,
|
||||
Link: jobLink(runID*10 + 2),
|
||||
JobID: "job-102",
|
||||
Name: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit",
|
||||
Status: actions_model.StatusFailure.String(),
|
||||
CanRerun: false,
|
||||
Duration: "3h35m10s",
|
||||
Needs: []string{"job-100", "job-101"},
|
||||
})
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: runID*10 + 3,
|
||||
Link: jobLink(runID*10 + 3),
|
||||
JobID: "job-103",
|
||||
Name: "job 103",
|
||||
Status: actions_model.StatusCancelled.String(),
|
||||
CanRerun: false,
|
||||
Duration: "2m",
|
||||
Needs: []string{"job-100"},
|
||||
})
|
||||
|
||||
// add more jobs to a run for UI testing
|
||||
if resp.State.Run.CanCancel {
|
||||
for i := range 10 {
|
||||
jobID := runID*1000 + int64(i)
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: jobID,
|
||||
Link: jobLink(jobID),
|
||||
JobID: "job-dup-test-" + strconv.Itoa(i),
|
||||
Name: "job dup test " + strconv.Itoa(i),
|
||||
Status: actions_model.StatusSuccess.String(),
|
||||
CanRerun: false,
|
||||
Duration: "2m",
|
||||
Needs: []string{"job-103", "job-101", "job-100"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if runID == 40 {
|
||||
// Reusable workflow caller demo: same-repo caller (with a nested same-repo caller inside),
|
||||
// alongside a flat cross-repo caller.
|
||||
// Layout:
|
||||
// prepare (regular, top-level)
|
||||
// local_caller (caller, same-repo, expanded)
|
||||
// ├ lib_step (regular)
|
||||
// └ inner_caller (caller, same-repo nested, expanded)
|
||||
// └ deep_job (regular)
|
||||
// cross_caller (caller, cross-repo, expanded)
|
||||
// └ external_job (regular)
|
||||
// build (linux|windows|macos) (regular matrix; graph folds into one "build" node)
|
||||
// build-call (linux|windows|macos) (caller matrix, each calls build.yml; folds into one "build-call" node like "build")
|
||||
// final (regular, needs local_caller + cross_caller)
|
||||
const (
|
||||
prepareID = int64(400)
|
||||
localCallerID = int64(401)
|
||||
libStepID = int64(402)
|
||||
innerCallerID = int64(403)
|
||||
deepJobID = int64(404)
|
||||
crossCallerID = int64(405)
|
||||
externalJobID = int64(406)
|
||||
finalID = int64(407)
|
||||
|
||||
// Regular matrix set – the graph already folds these into a single "build" node.
|
||||
buildLinuxID = int64(410)
|
||||
buildWindowsID = int64(411)
|
||||
buildMacosID = int64(412)
|
||||
|
||||
// Caller matrix set – each matrix leg calls the same reusable workflow. #38466: like the
|
||||
// regular "build" matrix above, these fold into one "build-call" node. Matrix legs share a
|
||||
// single JobID, so the legs below use JobID "build-call" and differ only by their name suffix.
|
||||
buildCallLinuxID = int64(420)
|
||||
buildCallWindowsID = int64(421)
|
||||
buildCallMacosID = int64(422)
|
||||
buildCallLinuxJobID = int64(423)
|
||||
buildCallWinJobID = int64(424)
|
||||
buildCallMacJobID = int64(425)
|
||||
)
|
||||
|
||||
resp.State.Run.Jobs = []*actions.ViewJob{
|
||||
{
|
||||
ID: prepareID, Link: jobLink(prepareID), JobID: "prepare", Name: "prepare",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "30s",
|
||||
},
|
||||
{
|
||||
ID: localCallerID, Link: jobLink(localCallerID), JobID: "local_caller", Name: "local caller",
|
||||
Status: actions_model.StatusRunning.String(), Duration: "5m",
|
||||
Needs: []string{"prepare"},
|
||||
IsReusableCaller: true, CallUses: "./.gitea/workflows/lib.yml",
|
||||
},
|
||||
{
|
||||
ID: libStepID, Link: jobLink(libStepID), JobID: "lib_step", Name: "lib step",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "1m",
|
||||
ParentJobID: localCallerID,
|
||||
},
|
||||
{
|
||||
ID: innerCallerID, Link: jobLink(innerCallerID), JobID: "inner_caller", Name: "inner caller (nested)",
|
||||
Status: actions_model.StatusRunning.String(), Duration: "4m",
|
||||
ParentJobID: localCallerID,
|
||||
IsReusableCaller: true, CallUses: "./.gitea/workflows/inner.yml",
|
||||
},
|
||||
{
|
||||
ID: deepJobID, Link: jobLink(deepJobID), JobID: "deep_job", Name: "deep job",
|
||||
Status: actions_model.StatusRunning.String(), Duration: "2m",
|
||||
ParentJobID: innerCallerID,
|
||||
},
|
||||
{
|
||||
ID: crossCallerID, Link: jobLink(crossCallerID), JobID: "cross_caller", Name: "cross-repo caller",
|
||||
Status: actions_model.StatusWaiting.String(), Duration: "0s",
|
||||
Needs: []string{"prepare"},
|
||||
IsReusableCaller: true, CallUses: "user2/lib-repo/.gitea/workflows/external.yml@main",
|
||||
},
|
||||
{
|
||||
ID: externalJobID, Link: jobLink(externalJobID), JobID: "external_job", Name: "external job",
|
||||
Status: actions_model.StatusWaiting.String(), Duration: "0s",
|
||||
ParentJobID: crossCallerID,
|
||||
},
|
||||
|
||||
// Regular matrix "build" – these fold into one matrix node in the graph. The matrix legs
|
||||
// share a single JobID ("build"); the " (variant)" name suffix distinguishes the legs.
|
||||
{
|
||||
ID: buildLinuxID, Link: jobLink(buildLinuxID), JobID: "build", Name: "build (linux)",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "1m", Needs: []string{"prepare"},
|
||||
},
|
||||
{
|
||||
ID: buildWindowsID, Link: jobLink(buildWindowsID), JobID: "build", Name: "build (windows)",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "2m", Needs: []string{"prepare"},
|
||||
},
|
||||
{
|
||||
ID: buildMacosID, Link: jobLink(buildMacosID), JobID: "build", Name: "build (macos)",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "90s", Needs: []string{"prepare"},
|
||||
},
|
||||
|
||||
// Caller matrix "build-call" – each leg calls the same reusable workflow. #38466: like the
|
||||
// regular "build" matrix above, these fold into one node. The matrix legs share a single
|
||||
// JobID ("build-call"); the " (variant)" name suffix distinguishes the legs.
|
||||
{
|
||||
ID: buildCallLinuxID, Link: jobLink(buildCallLinuxID), JobID: "build-call", Name: "build-call (linux)",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "1m", Needs: []string{"prepare"},
|
||||
IsReusableCaller: true, CallUses: "./.gitea/workflows/build.yml",
|
||||
},
|
||||
{
|
||||
ID: buildCallLinuxJobID, Link: jobLink(buildCallLinuxJobID), JobID: "bc_linux_build", Name: "build",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "1m", ParentJobID: buildCallLinuxID,
|
||||
},
|
||||
{
|
||||
ID: buildCallWindowsID, Link: jobLink(buildCallWindowsID), JobID: "build-call", Name: "build-call (windows)",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "2m", Needs: []string{"prepare"},
|
||||
IsReusableCaller: true, CallUses: "./.gitea/workflows/build.yml",
|
||||
},
|
||||
{
|
||||
ID: buildCallWinJobID, Link: jobLink(buildCallWinJobID), JobID: "bc_windows_build", Name: "build",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "2m", ParentJobID: buildCallWindowsID,
|
||||
},
|
||||
{
|
||||
ID: buildCallMacosID, Link: jobLink(buildCallMacosID), JobID: "build-call", Name: "build-call (macos)",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "90s", Needs: []string{"prepare"},
|
||||
IsReusableCaller: true, CallUses: "./.gitea/workflows/build.yml",
|
||||
},
|
||||
{
|
||||
ID: buildCallMacJobID, Link: jobLink(buildCallMacJobID), JobID: "bc_macos_build", Name: "build",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "90s", ParentJobID: buildCallMacosID,
|
||||
},
|
||||
|
||||
{
|
||||
ID: finalID, Link: jobLink(finalID), JobID: "final", Name: "final",
|
||||
Status: actions_model.StatusBlocked.String(), Duration: "0s",
|
||||
Needs: []string{"local_caller", "cross_caller"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fillViewRunResponseCurrentJob(ctx, resp)
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func fillViewRunResponseCurrentJob(ctx *context.Context, resp *actions.ViewResponse) {
|
||||
jobID := ctx.PathParamInt64("job")
|
||||
if jobID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, job := range resp.State.Run.Jobs {
|
||||
if job.ID == jobID {
|
||||
resp.State.CurrentJob.Title = job.Name
|
||||
resp.State.CurrentJob.Detail = job.Status
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
req := web.GetForm(ctx).(*actions.ViewRequest)
|
||||
var mockLogOptions []generateMockStepsLogOptions
|
||||
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{
|
||||
Summary: "step 0 (mock slow)",
|
||||
Duration: time.Hour.String(),
|
||||
Status: actions_model.StatusRunning.String(),
|
||||
})
|
||||
mockLogOptions = append(mockLogOptions, generateMockStepsLogOptions{mockCountFirst: 30, mockCountGeneral: 1, groupRepeat: 3})
|
||||
|
||||
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{
|
||||
Summary: "step 1 (mock fast)",
|
||||
Duration: time.Hour.String(),
|
||||
Status: actions_model.StatusRunning.String(),
|
||||
})
|
||||
mockLogOptions = append(mockLogOptions, generateMockStepsLogOptions{mockCountFirst: 30, mockCountGeneral: 3, groupRepeat: 20})
|
||||
|
||||
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{
|
||||
Summary: "step 2 (mock error)",
|
||||
Duration: time.Hour.String(),
|
||||
Status: actions_model.StatusRunning.String(),
|
||||
})
|
||||
mockLogOptions = append(mockLogOptions, generateMockStepsLogOptions{mockCountFirst: 30, mockCountGeneral: 3, groupRepeat: 3})
|
||||
|
||||
if len(req.LogCursors) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Logs.StepsLog = []*actions.ViewStepLog{}
|
||||
doSlowResponse := false
|
||||
doErrorResponse := false
|
||||
for _, logCur := range req.LogCursors {
|
||||
if !logCur.Expanded {
|
||||
continue
|
||||
}
|
||||
doSlowResponse = doSlowResponse || logCur.Step == 0
|
||||
doErrorResponse = doErrorResponse || logCur.Step == 2
|
||||
resp.Logs.StepsLog = append(resp.Logs.StepsLog, generateMockStepsLog(logCur, mockLogOptions[logCur.Step])...)
|
||||
}
|
||||
if doErrorResponse {
|
||||
if mathRand.Float64() > 0.5 {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "devtest mock error response")
|
||||
return
|
||||
}
|
||||
}
|
||||
if doSlowResponse {
|
||||
time.Sleep(time.Duration(3000) * time.Millisecond)
|
||||
} else {
|
||||
time.Sleep(time.Duration(100) * time.Millisecond) // actually, frontend reload every 1 second, any smaller delay is fine
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user