From dafc9e127aac1beb3078d17820f69c9f7ba629cd Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 29 May 2026 12:10:51 +0200 Subject: [PATCH 001/109] chore: update giteabot to v1.0.3 (#37896) Bump the pinned `giteabot` action to the [`v1.0.3`](https://github.com/go-gitea/giteabot/releases/tag/v1.0.3) release in both `giteabot.yml` and `giteabot-backport.yml`. v1.0.3 moves label/state queries off the search API on top of the existing retry logic. --- This PR was written with the help of Claude Opus 4.8 Co-authored-by: Claude (Opus 4.8) --- .github/workflows/giteabot-backport.yml | 2 +- .github/workflows/giteabot.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/giteabot-backport.yml b/.github/workflows/giteabot-backport.yml index 9a9c244b0c8..b90c225b4ba 100644 --- a/.github/workflows/giteabot-backport.yml +++ b/.github/workflows/giteabot-backport.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: go-gitea/giteabot@d4f19d5b4a88059d8c3ca78d660631506fc0c286 # add retry logic to giteabot + - uses: go-gitea/giteabot@f8a6f4c14d46920b4b5448852be3de72d00066f0 # v1.0.3 with: github_token: ${{ secrets.GITEABOT_TOKEN }} gitea_fork: giteabot/gitea diff --git a/.github/workflows/giteabot.yml b/.github/workflows/giteabot.yml index 91043988898..efa9fe047ae 100644 --- a/.github/workflows/giteabot.yml +++ b/.github/workflows/giteabot.yml @@ -45,7 +45,7 @@ jobs: steps: # pull_request_review runs without repository secrets on fork PRs, so fall # back to the workflow token for the non-backport checks handled here. - - uses: go-gitea/giteabot@d4f19d5b4a88059d8c3ca78d660631506fc0c286 # add retry logic to giteabot + - uses: go-gitea/giteabot@f8a6f4c14d46920b4b5448852be3de72d00066f0 # v1.0.3 with: github_token: ${{ secrets.GITEABOT_TOKEN || github.token }} checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }} From dd59c6848660ffa31db0db8c14c2a610076a7a8b Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 29 May 2026 22:16:47 +0200 Subject: [PATCH 002/109] feat(actions): bulk delete, disable and enable runners in admin UI (#37869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds bulk actions on the site-admin runner list (`/-/admin/actions/runners`). Site admins can now select multiple runners and **Delete**, **Disable**, or **Enable** them in one go instead of clicking through each runner's edit page. Scope is intentionally limited to the admin page. The user, org, and repo runner pages keep their existing per-row UX — the shared list template gates the bulk UI behind an `AllowBulkActions` flag set only by the admin handler. ## Screenshots --------- Signed-off-by: Nicolas Co-authored-by: wxiaoguang --- routers/web/shared/actions/runners.go | 72 +++++++++++++++++++ routers/web/web.go | 1 + templates/shared/actions/runner_list.tmpl | 29 ++++++-- .../integration/actions_runner_modify_test.go | 53 ++++++++++++++ web_src/js/features/admin/common.ts | 36 ++++++++++ web_src/js/features/common-fetch-action.ts | 4 +- 6 files changed, 189 insertions(+), 6 deletions(-) diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index 4b16237f605..f174bfb1cd4 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -4,6 +4,7 @@ package actions import ( + stdctx "context" "errors" "fmt" "net/http" @@ -158,6 +159,7 @@ func Runners(ctx *context.Context) { ctx.Data["RunnerOwnerID"] = opts.OwnerID ctx.Data["RunnerRepoID"] = opts.RepoID ctx.Data["SortType"] = opts.Sort + ctx.Data["AllowBulkActions"] = rCtx.IsAdmin pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) @@ -362,6 +364,76 @@ func RunnerUpdatePost(ctx *context.Context) { ctx.JSONRedirect("") } +// RunnerBulkActionPost performs a bulk action (delete/disable/enable) on multiple runners. +// Admin-only: route must be mounted inside the admin runners group; defense-in-depth check below. +func RunnerBulkActionPost(ctx *context.Context) { + rCtx, err := getRunnersCtx(ctx) + if err != nil { + ctx.ServerError("getRunnersCtx", err) + return + } + + var runnerIDs []int64 + if rCtx.IsAdmin { + // ATTENTION: it completely depends on the assumption that the doer is "site admin" + // So it doesn't do extra permission check to the runner IDs + // In the future, if you need to support such operation on non-admin pages, be careful! + runnerIDs = ctx.FormStringInt64s("ids") + } else { + ctx.HTTPError(http.StatusForbidden, "bulk actions are admin-only") + return + } + + action := ctx.FormString("action") + var successKey, failedKey string + switch action { + case "delete": + successKey, failedKey = "actions.runners.delete_runner_success", "actions.runners.delete_runner_failed" + case "disable": + successKey, failedKey = "actions.runners.disable_runner_success", "actions.runners.disable_runner_failed" + case "enable": + successKey, failedKey = "actions.runners.enable_runner_success", "actions.runners.enable_runner_failed" + default: + ctx.HTTPError(http.StatusBadRequest, "invalid action") + return + } + + runners, err := db.Find[actions_model.ActionRunner](ctx, &actions_model.FindRunnerOptions{IDs: runnerIDs}) + if err != nil { + ctx.ServerError("FindRunners", err) + return + } + + err = db.WithTx(ctx, func(txCtx stdctx.Context) error { + for _, r := range runners { + switch action { + case "delete": + if err := actions_model.DeleteRunner(txCtx, r.ID); err != nil { + return err + } + case "disable": + if err := actions_model.SetRunnerDisabled(txCtx, r, true); err != nil { + return err + } + case "enable": + if err := actions_model.SetRunnerDisabled(txCtx, r, false); err != nil { + return err + } + } + } + return nil + }) + if err != nil { + log.Warn("RunnerBulkActionPost.%s failed: %v, url: %s", action, err, ctx.Req.URL) + ctx.Flash.Error(ctx.Tr(failedKey)) + ctx.JSONRedirect(rCtx.RedirectLink) + return + } + + ctx.Flash.Success(ctx.Tr(successKey)) + ctx.JSONRedirect(rCtx.RedirectLink) +} + func findActionsRunner(ctx *context.Context, rCtx *runnersCtx) *actions_model.ActionRunner { runnerID := ctx.PathParamInt64("runnerid") opts := &actions_model.FindRunnerOptions{ diff --git a/routers/web/web.go b/routers/web/web.go index d02f7442649..49a83c1fae5 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -863,6 +863,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Group("/actions", func() { m.Get("", misc.LocationRedirect("./actions/runners")) addSettingsRunnersRoutes() + m.Post("/runners/bulk", shared_actions.RunnerBulkActionPost) addSettingsVariablesRoutes() }) }, adminReq, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled)) diff --git a/templates/shared/actions/runner_list.tmpl b/templates/shared/actions/runner_list.tmpl index 90eb4591d7a..1b287cdabc2 100644 --- a/templates/shared/actions/runner_list.tmpl +++ b/templates/shared/actions/runner_list.tmpl @@ -40,10 +40,28 @@ {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.runner_kind")}} + {{if .AllowBulkActions}} +
+
+ + + + +
+
+ {{end}} + {{if .Runners}}
+ {{if .AllowBulkActions}} + + {{end}} {{range .Runners}} + {{if $.AllowBulkActions}} + + {{end}} - {{else}} - - - {{end}}
{{ctx.Locale.Tr "actions.runners.status"}} {{SortArrow "online" "offline" .SortType false}} @@ -66,6 +84,9 @@
{{.StatusLocaleName ctx.Locale}} {{if .IsDisabled}}{{ctx.Locale.Tr "actions.runners.disabled"}}{{end}} @@ -84,15 +105,13 @@ {{end}}
{{ctx.Locale.Tr "actions.runners.none"}}
+ {{else}} +
{{ctx.Locale.Tr "actions.runners.none"}}
+ {{end}} {{template "base/paginate" .}} - diff --git a/tests/integration/actions_runner_modify_test.go b/tests/integration/actions_runner_modify_test.go index 4fffbddeb26..f9614d69873 100644 --- a/tests/integration/actions_runner_modify_test.go +++ b/tests/integration/actions_runner_modify_test.go @@ -6,6 +6,7 @@ package integration import ( "fmt" "net/http" + "strings" "testing" actions_model "gitea.dev/models/actions" @@ -13,6 +14,7 @@ import ( repo_model "gitea.dev/models/repo" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" + "gitea.dev/modules/base" "gitea.dev/tests" "github.com/stretchr/testify/assert" @@ -163,4 +165,55 @@ func TestActionsRunnerModify(t *testing.T) { assertSuccess(t, sessionAdmin, adminWebURL, globalRunner.ID) }) }) + + t.Run("BulkAction", func(t *testing.T) { + // Previous subtests deleted all runners; create a fresh set scoped to this subtest. + require.NoError(t, actions_model.CreateRunner(ctx, &actions_model.ActionRunner{Name: "bulk-runner-1", TokenHash: "e", UUID: "e"})) + require.NoError(t, actions_model.CreateRunner(ctx, &actions_model.ActionRunner{Name: "bulk-runner-2", TokenHash: "f", UUID: "f"})) + require.NoError(t, actions_model.CreateRunner(ctx, &actions_model.ActionRunner{Name: "bulk-runner-3", TokenHash: "g", UUID: "g"})) + r1 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{Name: "bulk-runner-1"}) + r2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{Name: "bulk-runner-2"}) + r3 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{Name: "bulk-runner-3"}) + allIDs := []int64{r1.ID, r2.ID, r3.ID} + bulkURL := adminWebURL + "/bulk" + doBulk := func(t *testing.T, sess *TestSession, action string, ids []int64, expectedStatus int) { + req := NewRequestWithValues(t, "POST", bulkURL, map[string]string{ + "action": action, + "ids": strings.Join(base.Int64sToStrings(ids), ","), + }) + sess.MakeRequest(t, req, expectedStatus) + } + + t.Run("NonAdminForbidden", func(t *testing.T) { + doBulk(t, sessionUser2, "disable", allIDs, http.StatusForbidden) + for _, id := range allIDs { + v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id}) + assert.False(t, v.IsDisabled, "runner %d should not have been disabled", id) + } + }) + + t.Run("InvalidAction", func(t *testing.T) { + doBulk(t, sessionAdmin, "evict", allIDs, http.StatusBadRequest) + }) + + t.Run("DisableEnable", func(t *testing.T) { + doBulk(t, sessionAdmin, "disable", allIDs, http.StatusOK) + for _, id := range allIDs { + v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id}) + assert.True(t, v.IsDisabled, "runner %d should be disabled", id) + } + doBulk(t, sessionAdmin, "enable", allIDs, http.StatusOK) + for _, id := range allIDs { + v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id}) + assert.False(t, v.IsDisabled, "runner %d should be enabled", id) + } + }) + + t.Run("Delete", func(t *testing.T) { + doBulk(t, sessionAdmin, "delete", allIDs, http.StatusOK) + for _, id := range allIDs { + unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: id}) + } + }) + }) } diff --git a/web_src/js/features/admin/common.ts b/web_src/js/features/admin/common.ts index 5753aad2b2c..6125578a7a0 100644 --- a/web_src/js/features/admin/common.ts +++ b/web_src/js/features/admin/common.ts @@ -3,6 +3,7 @@ import {hideElem, queryElems, showElem, toggleElem} from '../../utils/dom.ts'; import {POST} from '../../modules/fetch.ts'; import {showFomanticModal} from '../../modules/fomantic/modal.ts'; import {pathEscape} from '../../utils/url.ts'; +import {registerGlobalInitFunc} from '../../modules/observer.ts'; const {appSubUrl} = window.config; @@ -23,6 +24,41 @@ export function initAdminCommon(): void { initAdminUser(); initAdminAuthentication(); initAdminNotice(); + registerGlobalInitFunc('initRunnerBulkToolbar', initAdminRunnerBulk); +} + +function initAdminRunnerBulk(toolbar: HTMLElement) { + const actionButtons = toolbar.querySelectorAll('.runner-bulk-action'); + const formRunnerIds = toolbar.querySelector('form input[name="ids"]')!; + const rowCheckboxes = document.querySelectorAll('.runner-bulk-select'); + const selectAll = document.querySelector('.runner-bulk-select-all'); + if (!selectAll) return; + + const refresh = () => { + const checked = Array.from(rowCheckboxes).filter((c) => c.checked); + toggleElem(toolbar, checked.length > 0); + for (const btn of actionButtons) { + btn.querySelector('.runner-bulk-count')!.textContent = `(${checked.length})`; + } + selectAll.checked = checked.length > 0 && checked.length === rowCheckboxes.length; + selectAll.indeterminate = checked.length > 0 && checked.length < rowCheckboxes.length; + }; + + selectAll.addEventListener('change', () => { + for (const cb of rowCheckboxes) cb.checked = selectAll.checked; + refresh(); + }); + for (const cb of rowCheckboxes) cb.addEventListener('change', refresh); + refresh(); + + const collectSelectedIds = () => { + const ids = []; + for (const cb of rowCheckboxes) { + if (cb.checked) ids.push(cb.getAttribute('data-runner-id')!); + } + return ids.join(','); + }; + formRunnerIds.value = collectSelectedIds(); } function initAdminUser() { diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts index 0f65f780acb..3481d240f27 100644 --- a/web_src/js/features/common-fetch-action.ts +++ b/web_src/js/features/common-fetch-action.ts @@ -16,6 +16,7 @@ type FetchActionOpts = { url: string; headers?: HeadersInit; body?: FormData; + formSubmitter?: HTMLElement | null; // pseudo selectors/commands to update the current page with the response text when the response is text (html) // e.g.: "$this", "$innerHTML", "$closest(tr) td .the-class", "$body #the-id" @@ -122,7 +123,7 @@ function buildFetchActionUrl(el: HTMLElement, opt: FetchActionOpts) { async function performActionRequest(el: HTMLElement, opt: FetchActionOpts) { const attrIsLoading = 'data-fetch-is-loading'; if (el.getAttribute(attrIsLoading)) return; - if (!await confirmFetchAction(el)) return; + if (!await confirmFetchAction(opt.formSubmitter ?? el)) return; el.setAttribute(attrIsLoading, 'true'); toggleLoadingIndicator(el, opt, true); @@ -181,6 +182,7 @@ function prepareFormFetchActionOpts(formEl: HTMLFormElement, opts: SubmitFormFet method: formMethodUpper, url: reqUrl, body: reqBody, + formSubmitter: opts.formSubmitter, loadingIndicator: '$this', // for form submit, by default, the loading indicator is the whole form successSync: formEl.getAttribute('data-fetch-sync') ?? '', // by default, no fetch sync for form submit }; From d07a42e7774abca9c381ef6a8c361a637ba93523 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Fri, 29 May 2026 15:04:40 -0700 Subject: [PATCH 003/109] fix(deps): update module golang.org/x/image to v0.41.0 [security] (#37904) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) | [`v0.40.0` → `v0.41.0`](https://cs.opensource.google/go/x/image/+/refs/tags/v0.40.0...refs/tags/v0.41.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fimage/v0.41.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fimage/v0.40.0/v0.41.0?slim=true) | --- ### Panic when reading out of bound palette index in golang.org/x/image/bmp [CVE-2026-42500](https://nvd.nist.gov/vuln/detail/CVE-2026-42500) / [GO-2026-5031](https://pkg.go.dev/vuln/GO-2026-5031)
More information #### Details Decoding a paletted BMP file with an out-of-range palette index results in a panic when accessing pixels in the invalid image. #### Severity Unknown #### References - [https://go.dev/issue/79576](https://go.dev/issue/79576) - [https://groups.google.com/g/golang-announce/c/uhYX90BlBvI](https://groups.google.com/g/golang-announce/c/uhYX90BlBvI) - [https://go.dev/cl/781500](https://go.dev/cl/781500) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5031) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- ### Excessive resource consumption in PackBits decompression in golang.org/x/image/tiff [CVE-2026-46599](https://nvd.nist.gov/vuln/detail/CVE-2026-46599) / [GO-2026-5032](https://pkg.go.dev/vuln/GO-2026-5032)
More information #### Details The TIFF decoder does not place a limit on the size of PackBits-compressed data. A maliciously-crafted image can exploit this to cause a small image (both in terms of pixel width/height and encoded size) to make the decoder decode large amounts of compressed data. #### Severity Unknown #### References - [https://go.dev/issue/79577](https://go.dev/issue/79577) - [https://go.dev/cl/759960](https://go.dev/cl/759960) - [https://groups.google.com/g/golang-announce/c/uhYX90BlBvI](https://groups.google.com/g/golang-announce/c/uhYX90BlBvI) This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5032) and the [Go Vulnerability Database](https://redirect.github.com/golang/vulndb) ([CC-BY 4.0](https://redirect.github.com/golang/vulndb#license)).
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - "" - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0b8d4f977a7..e60b507a4bd 100644 --- a/go.mod +++ b/go.mod @@ -104,7 +104,7 @@ require ( gitlab.com/gitlab-org/api/client-go/v2 v2.30.0 go.yaml.in/yaml/v4 v4.0.0-rc.3 golang.org/x/crypto v0.52.0 - golang.org/x/image v0.40.0 + golang.org/x/image v0.41.0 golang.org/x/net v0.55.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 diff --git a/go.sum b/go.sum index 3038c46e012..fb1e1463d38 100644 --- a/go.sum +++ b/go.sum @@ -793,8 +793,8 @@ golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= -golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8= -golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= +golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= +golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= From a342206a21c447f571d4db51c602c72be7c3a76a Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 30 May 2026 01:50:55 +0200 Subject: [PATCH 004/109] fix(locales): Replace hardcoded strings (#37788) The Workflow Dependencies graph in the Actions run details view had hard-coded English strings. Also in projects view and contributors view I found some hard-coded strings. The other items in the issue #37787 (Summary / All jobs / Run Details / Workflow file / Triggered via / Total duration) were already wired through ctx.Locale.Tr; their translations just need to land in the non-English locale_*.json files via the translation pipeline. Fixes #37787 --------- Co-authored-by: silverwind Co-authored-by: Claude (Opus 4.8) --- options/locale/locale_en-US.json | 11 ++++++++ templates/projects/view.tmpl | 8 +++--- templates/repo/actions/view_component.tmpl | 10 +++++++ templates/repo/contributors.tmpl | 1 + .../js/components/ActionRunSummaryView.vue | 1 + web_src/js/components/RepoContributors.vue | 2 +- web_src/js/components/WorkflowGraph.vue | 28 +++++++++---------- web_src/js/features/contributors.ts | 1 + web_src/js/features/repo-actions.ts | 10 +++++++ web_src/js/modules/i18n.test.ts | 15 ++++++++++ web_src/js/modules/i18n.ts | 7 +++++ 11 files changed, 74 insertions(+), 20 deletions(-) create mode 100644 web_src/js/modules/i18n.test.ts create mode 100644 web_src/js/modules/i18n.ts diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 1ba431c5eae..ec564d3e262 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2725,6 +2725,7 @@ "graphs.code_frequency.what": "code frequency", "graphs.contributors.what": "contributions", "graphs.recent_commits.what": "recent commits", + "graphs.chart_zoom_hint": "drag: zoom, shift+drag: pan, double click: reset zoom", "org.org_name_holder": "Organization Name", "org.org_full_name_holder": "Organization Full Name", "org.org_name_helper": "Organization names should be short and memorable.", @@ -3797,6 +3798,16 @@ "actions.runs.latest_attempt": "Latest attempt", "actions.runs.triggered_via": "Triggered via %s", "actions.runs.total_duration": "Total duration:", + "actions.runs.workflow_dependencies": "Workflow Dependencies", + "actions.runs.graph_jobs_count_1": "%d job", + "actions.runs.graph_jobs_count_n": "%d jobs", + "actions.runs.graph_dependencies_count_1": "%d dependency", + "actions.runs.graph_dependencies_count_n": "%d dependencies", + "actions.runs.graph_success_rate": "%s success", + "actions.runs.graph_zoom_in": "Zoom in (Ctrl/Cmd + scroll on graph)", + "actions.runs.graph_zoom_max": "Already at 100% zoom", + "actions.runs.graph_zoom_out": "Zoom out (Ctrl/Cmd + scroll on graph)", + "actions.runs.graph_reset_view": "Reset view", "actions.workflow.disable": "Disable Workflow", "actions.workflow.disable_success": "Workflow '%s' disabled successfully.", "actions.workflow.enable": "Enable Workflow", diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 30056e211f1..8f4f1d60fda 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -134,16 +134,16 @@ {{if $canWriteProject}} {{end}} diff --git a/web_src/js/components/ActionRunSummaryView.vue b/web_src/js/components/ActionRunSummaryView.vue index afbc0a13bf1..6402c2465b7 100644 --- a/web_src/js/components/ActionRunSummaryView.vue +++ b/web_src/js/components/ActionRunSummaryView.vue @@ -59,6 +59,7 @@ onBeforeUnmount(() => { :jobs="run.jobs" :run-link="run.link" :workflow-id="run.workflowID" + :locale="locale" /> diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue index 3fd34081e04..d4d5190c6e6 100644 --- a/web_src/js/components/RepoContributors.vue +++ b/web_src/js/components/RepoContributors.vue @@ -270,7 +270,7 @@ export default defineComponent({ plugins: { title: { display: type === 'main', - text: 'drag: zoom, shift+drag: pan, double click: reset zoom', + text: this.locale.chartZoomHint, position: 'top', align: 'center', }, diff --git a/web_src/js/components/WorkflowGraph.vue b/web_src/js/components/WorkflowGraph.vue index 3cca254dda2..c01226f2a91 100644 --- a/web_src/js/components/WorkflowGraph.vue +++ b/web_src/js/components/WorkflowGraph.vue @@ -4,6 +4,7 @@ import {SvgIcon} from '../svg.ts'; import ActionStatusIcon from './ActionStatusIcon.vue'; import {localUserSettings} from '../modules/user-settings.ts'; import {isPlainClick} from '../utils/dom.ts'; +import {trN} from '../modules/i18n.ts'; import {debounce} from 'throttle-debounce'; import type {ActionsJob, ActionsStatus} from '../modules/gitea-actions.ts'; import type {ActionRunViewStore} from './ActionRunView.ts'; @@ -43,6 +44,7 @@ const props = defineProps<{ jobs: ActionsJob[]; runLink: string; workflowId: string; + locale: Record; }>() const settingKeyStates = 'actions-graph-states'; @@ -344,6 +346,12 @@ const graphMetrics = computed(() => { }; }) +const graphStats = computed(() => [ + trN(props.jobs.length, props.locale.graphJobsCount1, props.locale.graphJobsCountN), + trN(edges.value.length, props.locale.graphDependenciesCount1, props.locale.graphDependenciesCountN), + props.locale.graphSuccessRate.replace('%s', graphMetrics.value.successRate), +].join(' • ')) + const nodeHeight = 52; const verticalSpacing = 90; const margin = 40; @@ -543,27 +551,22 @@ function onNodeClick(job: JobNode, event: MouseEvent) {