diff --git a/.golangci.yml b/.golangci.yml
index 37617ad365..c39d7ac5f2 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -19,6 +19,8 @@ linters:
     - revive
     - staticcheck
     - stylecheck
+    - tenv
+    - testifylint
     - typecheck
     - unconvert
     - unused
@@ -34,6 +36,10 @@ output:
   show-stats: true
 
 linters-settings:
+  testifylint:
+    disable:
+      - go-require
+      - require-error
   stylecheck:
     checks: ["all", "-ST1005", "-ST1003"]
   nakedret:
diff --git a/MAINTAINERS b/MAINTAINERS
index 426181cbcf..ad02ecc755 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -46,7 +46,6 @@ Wim <wim@42.be> (@42wim)
 Jason Song <i@wolfogre.com> (@wolfogre)
 Yarden Shoham <git@yardenshoham.com> (@yardenshoham)
 Yu Tian <zettat123@gmail.com> (@Zettat123)
-Eddie Yang <576951401@qq.com> (@yp05327)
 Dong Ge <gedong_1994@163.com> (@sillyguodong)
 Xinyi Gong <hestergong@gmail.com> (@HesterG)
 wxiaoguang <wxiaoguang@gmail.com> (@wxiaoguang)
diff --git a/Makefile b/Makefile
index 0cd6936b3b..4889958c3b 100644
--- a/Makefile
+++ b/Makefile
@@ -28,7 +28,7 @@ XGO_VERSION := go-1.23.x
 AIR_PACKAGE ?= github.com/air-verse/air@v1
 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0
 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0
-GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.3
+GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2
 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.5.1
 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0
@@ -377,12 +377,12 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
 .PHONY: lint-js
 lint-js: node_modules
 	npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES)
-#	npx vue-tsc
+	npx vue-tsc
 
 .PHONY: lint-js-fix
 lint-js-fix: node_modules
 	npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix
-#	npx vue-tsc
+	npx vue-tsc
 
 .PHONY: lint-css
 lint-css: node_modules
@@ -451,10 +451,6 @@ lint-templates: .venv node_modules
 lint-yaml: .venv
 	@poetry run yamllint .
 
-.PHONY: tsc
-tsc:
-	npx vue-tsc
-
 .PHONY: watch
 watch:
 	@bash tools/watch.sh
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 5c23f70d7c..6377ebf9d2 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1040,9 +1040,13 @@ LEVEL = Info
 ;; Don't allow download source archive files from UI
 ;DISABLE_DOWNLOAD_SOURCE_ARCHIVES = false
 
-;; Allow fork repositories without maximum number limit
+;; Allow to fork repositories without maximum number limit
 ;ALLOW_FORK_WITHOUT_MAXIMUM_LIMIT = true
 
+;; Allow to fork repositories into the same owner (user or organization)
+;; This feature is experimental, not fully tested, and may be changed in the future
+;ALLOW_FORK_INTO_SAME_OWNER = false
+
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;[repository.editor]
diff --git a/go.mod b/go.mod
index 17be4cbd52..671151d4b6 100644
--- a/go.mod
+++ b/go.mod
@@ -48,7 +48,7 @@ require (
 	github.com/ethantkoenig/rupture v1.0.1
 	github.com/felixge/fgprof v0.9.5
 	github.com/fsnotify/fsnotify v1.7.0
-	github.com/gliderlabs/ssh v0.3.7
+	github.com/gliderlabs/ssh v0.3.8
 	github.com/go-ap/activitypub v0.0.0-20240910141749-b4b8c8aa484c
 	github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
 	github.com/go-chi/chi/v5 v5.1.0
diff --git a/go.sum b/go.sum
index 73bdb44e33..afa3abece8 100644
--- a/go.sum
+++ b/go.sum
@@ -293,8 +293,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv
 github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
 github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0=
 github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A=
-github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
-github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
+github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
+github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
 github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
 github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
 github.com/go-ap/activitypub v0.0.0-20240910141749-b4b8c8aa484c h1:82lzmsy5Nr6JA6HcLRVxGfbdSoWfW45C6jnY3zFS7Ks=
diff --git a/models/actions/run_job.go b/models/actions/run_job.go
index 4b8664077d..de4b6aab66 100644
--- a/models/actions/run_job.go
+++ b/models/actions/run_job.go
@@ -137,7 +137,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
 		if err != nil {
 			return 0, err
 		}
-		run.Status = aggregateJobStatus(jobs)
+		run.Status = AggregateJobStatus(jobs)
 		if run.Started.IsZero() && run.Status.IsRunning() {
 			run.Started = timeutil.TimeStampNow()
 		}
@@ -152,29 +152,35 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
 	return affected, nil
 }
 
-func aggregateJobStatus(jobs []*ActionRunJob) Status {
-	allDone := true
-	allWaiting := true
-	hasFailure := false
+func AggregateJobStatus(jobs []*ActionRunJob) Status {
+	allSuccessOrSkipped := len(jobs) != 0
+	allSkipped := len(jobs) != 0
+	var hasFailure, hasCancelled, 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)
+		allSkipped = allSkipped && job.Status == StatusSkipped
+		hasFailure = hasFailure || job.Status == StatusFailure
+		hasCancelled = hasCancelled || job.Status == StatusCancelled
+		hasWaiting = hasWaiting || job.Status == StatusWaiting
+		hasRunning = hasRunning || job.Status == StatusRunning
+		hasBlocked = hasBlocked || job.Status == StatusBlocked
 	}
-	if allDone {
-		if hasFailure {
-			return StatusFailure
-		}
+	switch {
+	case allSkipped:
+		return StatusSkipped
+	case allSuccessOrSkipped:
 		return StatusSuccess
-	}
-	if allWaiting {
+	case hasCancelled:
+		return StatusCancelled
+	case hasFailure:
+		return StatusFailure
+	case hasRunning:
+		return StatusRunning
+	case hasWaiting:
 		return StatusWaiting
+	case hasBlocked:
+		return StatusBlocked
+	default:
+		return StatusUnknown // it shouldn't happen
 	}
-	return StatusRunning
 }
diff --git a/models/actions/run_job_status_test.go b/models/actions/run_job_status_test.go
new file mode 100644
index 0000000000..04fd9ceba7
--- /dev/null
+++ b/models/actions/run_job_status_test.go
@@ -0,0 +1,85 @@
+// 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) {
+		t.Helper()
+		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
+	}{
+		// unknown cases, maybe it shouldn't happen in real world
+		{[]Status{}, StatusUnknown},
+		{[]Status{StatusUnknown, StatusSuccess}, StatusUnknown},
+		{[]Status{StatusUnknown, StatusSkipped}, StatusUnknown},
+		{[]Status{StatusUnknown, StatusFailure}, StatusFailure},
+		{[]Status{StatusUnknown, StatusCancelled}, StatusCancelled},
+		{[]Status{StatusUnknown, StatusWaiting}, StatusWaiting},
+		{[]Status{StatusUnknown, StatusRunning}, StatusRunning},
+		{[]Status{StatusUnknown, StatusBlocked}, StatusBlocked},
+
+		// 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},
+
+		// any cancelled, then cancelled
+		{[]Status{StatusCancelled}, StatusCancelled},
+		{[]Status{StatusCancelled, StatusSuccess}, StatusCancelled},
+		{[]Status{StatusCancelled, StatusSkipped}, StatusCancelled},
+		{[]Status{StatusCancelled, StatusFailure}, StatusCancelled},
+		{[]Status{StatusCancelled, StatusWaiting}, StatusCancelled},
+		{[]Status{StatusCancelled, StatusRunning}, StatusCancelled},
+		{[]Status{StatusCancelled, StatusBlocked}, StatusCancelled},
+
+		// 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}, StatusCancelled},
+		{[]Status{StatusFailure, StatusWaiting}, StatusFailure},
+		{[]Status{StatusFailure, StatusRunning}, StatusFailure},
+		{[]Status{StatusFailure, StatusBlocked}, StatusFailure},
+
+		// skipped with other status
+		// TODO: need to clarify whether a PR with "skipped" job status is considered as "mergeable" or not.
+		{[]Status{StatusSkipped}, StatusSkipped},
+		{[]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)
+	}
+}
diff --git a/models/actions/runner_token_test.go b/models/actions/runner_token_test.go
index e85e99abe5..159805e5f7 100644
--- a/models/actions/runner_token_test.go
+++ b/models/actions/runner_token_test.go
@@ -17,7 +17,7 @@ func TestGetLatestRunnerToken(t *testing.T) {
 	token := unittest.AssertExistsAndLoadBean(t, &ActionRunnerToken{ID: 3})
 	expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0)
 	assert.NoError(t, err)
-	assert.EqualValues(t, token, expectedToken)
+	assert.EqualValues(t, expectedToken, token)
 }
 
 func TestNewRunnerToken(t *testing.T) {
@@ -26,7 +26,7 @@ func TestNewRunnerToken(t *testing.T) {
 	assert.NoError(t, err)
 	expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0)
 	assert.NoError(t, err)
-	assert.EqualValues(t, token, expectedToken)
+	assert.EqualValues(t, expectedToken, token)
 }
 
 func TestUpdateRunnerToken(t *testing.T) {
@@ -36,5 +36,5 @@ func TestUpdateRunnerToken(t *testing.T) {
 	assert.NoError(t, UpdateRunnerToken(db.DefaultContext, token))
 	expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0)
 	assert.NoError(t, err)
-	assert.EqualValues(t, token, expectedToken)
+	assert.EqualValues(t, expectedToken, token)
 }
diff --git a/models/activities/user_heatmap_test.go b/models/activities/user_heatmap_test.go
index b7babcbde1..a039fd3613 100644
--- a/models/activities/user_heatmap_test.go
+++ b/models/activities/user_heatmap_test.go
@@ -4,7 +4,6 @@
 package activities_test
 
 import (
-	"fmt"
 	"testing"
 	"time"
 
@@ -91,11 +90,11 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
 		assert.NoError(t, err)
 		assert.Len(t, actions, contributions, "invalid action count: did the test data became too old?")
 		assert.Equal(t, count, int64(contributions))
-		assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase '%s'", tc.desc))
+		assert.Equal(t, tc.CountResult, contributions, "testcase '%s'", tc.desc)
 
 		// Test JSON rendering
 		jsonData, err := json.Marshal(heatmap)
 		assert.NoError(t, err)
-		assert.Equal(t, tc.JSONResult, string(jsonData))
+		assert.JSONEq(t, tc.JSONResult, string(jsonData))
 	}
 }
diff --git a/models/auth/oauth2_test.go b/models/auth/oauth2_test.go
index 0829d31d51..43daa0b5ec 100644
--- a/models/auth/oauth2_test.go
+++ b/models/auth/oauth2_test.go
@@ -18,7 +18,7 @@ func TestOAuth2Application_GenerateClientSecret(t *testing.T) {
 	app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
 	secret, err := app.GenerateClientSecret(db.DefaultContext)
 	assert.NoError(t, err)
-	assert.True(t, len(secret) > 0)
+	assert.NotEmpty(t, secret)
 	unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1, ClientSecret: app.ClientSecret})
 }
 
@@ -165,7 +165,7 @@ func TestOAuth2Grant_GenerateNewAuthorizationCode(t *testing.T) {
 	code, err := grant.GenerateNewAuthorizationCode(db.DefaultContext, "https://example2.com/callback", "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", "S256")
 	assert.NoError(t, err)
 	assert.NotNil(t, code)
-	assert.True(t, len(code.Code) > 32) // secret length > 32
+	assert.Greater(t, len(code.Code), 32) // secret length > 32
 }
 
 func TestOAuth2Grant_TableName(t *testing.T) {
diff --git a/models/db/iterate_test.go b/models/db/iterate_test.go
index 0f6ba2cc94..e9f2790671 100644
--- a/models/db/iterate_test.go
+++ b/models/db/iterate_test.go
@@ -38,8 +38,6 @@ func TestIterate(t *testing.T) {
 		if !has {
 			return db.ErrNotExist{Resource: "repo_unit", ID: repoUnit.ID}
 		}
-		assert.EqualValues(t, repoUnit.RepoID, repoUnit.RepoID)
-		assert.EqualValues(t, repoUnit.CreatedUnix, repoUnit.CreatedUnix)
 		return nil
 	})
 	assert.NoError(t, err)
diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml
index a42ab77ca5..1db849352f 100644
--- a/models/fixtures/action_run.yml
+++ b/models/fixtures/action_run.yml
@@ -36,3 +36,41 @@
   updated: 1683636626
   need_approval: 0
   approved_by: 0
+-
+  id: 793
+  title: "job output"
+  repo_id: 4
+  owner_id: 1
+  workflow_id: "test.yaml"
+  index: 189
+  trigger_user_id: 1
+  ref: "refs/heads/master"
+  commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
+  event: "push"
+  is_fork_pull_request: 0
+  status: 1
+  started: 1683636528
+  stopped: 1683636626
+  created: 1683636108
+  updated: 1683636626
+  need_approval: 0
+  approved_by: 0
+-
+  id: 794
+  title: "job output"
+  repo_id: 4
+  owner_id: 1
+  workflow_id: "test.yaml"
+  index: 190
+  trigger_user_id: 1
+  ref: "refs/heads/test"
+  commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
+  event: "push"
+  is_fork_pull_request: 0
+  status: 1
+  started: 1683636528
+  stopped: 1683636626
+  created: 1683636108
+  updated: 1683636626
+  need_approval: 0
+  approved_by: 0
diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml
index fd90f4fd5d..9b6f5b9a88 100644
--- a/models/fixtures/action_run_job.yml
+++ b/models/fixtures/action_run_job.yml
@@ -26,3 +26,46 @@
   status: 1
   started: 1683636528
   stopped: 1683636626
+-
+  id: 194
+  run_id: 793
+  repo_id: 4
+  owner_id: 1
+  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
+  is_fork_pull_request: 0
+  name: job1 (1)
+  attempt: 1
+  job_id: job1
+  task_id: 49
+  status: 1
+  started: 1683636528
+  stopped: 1683636626
+-
+  id: 195
+  run_id: 793
+  repo_id: 4
+  owner_id: 1
+  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
+  is_fork_pull_request: 0
+  name: job1 (2)
+  attempt: 1
+  job_id: job1
+  task_id: 50
+  status: 1
+  started: 1683636528
+  stopped: 1683636626
+-
+  id: 196
+  run_id: 793
+  repo_id: 4
+  owner_id: 1
+  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
+  is_fork_pull_request: 0
+  name: job2
+  attempt: 1
+  job_id: job2
+  needs: [job1]
+  task_id: 51
+  status: 5
+  started: 1683636528
+  stopped: 1683636626
diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml
index d88a8ed8a9..506a47d8a0 100644
--- a/models/fixtures/action_task.yml
+++ b/models/fixtures/action_task.yml
@@ -57,3 +57,63 @@
   log_length: 707
   log_size: 90179
   log_expired: 0
+-
+  id: 49
+  job_id: 194
+  attempt: 1
+  runner_id: 1
+  status: 1 # success
+  started: 1683636528
+  stopped: 1683636626
+  repo_id: 4
+  owner_id: 1
+  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
+  is_fork_pull_request: 0
+  token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784220
+  token_salt: ffffffffff
+  token_last_eight: ffffffff
+  log_filename: artifact-test2/2f/47.log
+  log_in_storage: 1
+  log_length: 707
+  log_size: 90179
+  log_expired: 0
+-
+  id: 50
+  job_id: 195
+  attempt: 1
+  runner_id: 1
+  status: 1 # success
+  started: 1683636528
+  stopped: 1683636626
+  repo_id: 4
+  owner_id: 1
+  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
+  is_fork_pull_request: 0
+  token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784221
+  token_salt: ffffffffff
+  token_last_eight: ffffffff
+  log_filename: artifact-test2/2f/47.log
+  log_in_storage: 1
+  log_length: 707
+  log_size: 90179
+  log_expired: 0
+-
+  id: 51
+  job_id: 196
+  attempt: 1
+  runner_id: 1
+  status: 6 # running
+  started: 1683636528
+  stopped: 1683636626
+  repo_id: 4
+  owner_id: 1
+  commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
+  is_fork_pull_request: 0
+  token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784222
+  token_salt: ffffffffff
+  token_last_eight: ffffffff
+  log_filename: artifact-test2/2f/47.log
+  log_in_storage: 1
+  log_length: 707
+  log_size: 90179
+  log_expired: 0
diff --git a/models/fixtures/action_task_output.yml b/models/fixtures/action_task_output.yml
new file mode 100644
index 0000000000..314e9f7115
--- /dev/null
+++ b/models/fixtures/action_task_output.yml
@@ -0,0 +1,20 @@
+-
+  id: 1
+  task_id: 49
+  output_key: output_a
+  output_value: abc
+-
+  id: 2
+  task_id: 49
+  output_key: output_b
+  output_value: ''
+-
+  id: 3
+  task_id: 50
+  output_key: output_a
+  output_value: ''
+-
+  id: 4
+  task_id: 50
+  output_key: output_b
+  output_value: bbb
diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml
index c7bdff7733..17b1869ab6 100644
--- a/models/fixtures/branch.yml
+++ b/models/fixtures/branch.yml
@@ -81,3 +81,15 @@
   is_deleted: false
   deleted_by_id: 0
   deleted_unix: 0
+
+-
+  id: 15
+  repo_id: 4
+  name: 'master'
+  commit_id: 'c7cd3cd144e6d23c9d6f3d07e52b2c1a956e0338'
+  commit_message: 'add Readme'
+  commit_time: 1588147171
+  pusher_id: 13
+  is_deleted: false
+  deleted_by_id: 0
+  deleted_unix: 0
diff --git a/models/git/commit_status_test.go b/models/git/commit_status_test.go
index 7ac4da6810..37d785e938 100644
--- a/models/git/commit_status_test.go
+++ b/models/git/commit_status_test.go
@@ -34,7 +34,7 @@ func TestGetCommitStatuses(t *testing.T) {
 		SHA:         sha1,
 	})
 	assert.NoError(t, err)
-	assert.Equal(t, int(maxResults), 5)
+	assert.Equal(t, 5, int(maxResults))
 	assert.Len(t, statuses, 5)
 
 	assert.Equal(t, "ci/awesomeness", statuses[0].Context)
@@ -63,7 +63,7 @@ func TestGetCommitStatuses(t *testing.T) {
 		SHA:         sha1,
 	})
 	assert.NoError(t, err)
-	assert.Equal(t, int(maxResults), 5)
+	assert.Equal(t, 5, int(maxResults))
 	assert.Empty(t, statuses)
 }
 
diff --git a/models/git/protected_branch_test.go b/models/git/protected_branch_test.go
index 49d433f845..e1c91d927d 100644
--- a/models/git/protected_branch_test.go
+++ b/models/git/protected_branch_test.go
@@ -4,7 +4,6 @@
 package git
 
 import (
-	"fmt"
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
@@ -76,7 +75,7 @@ func TestBranchRuleMatch(t *testing.T) {
 			infact = " not"
 		}
 		assert.EqualValues(t, kase.ExpectedMatch, pb.Match(kase.BranchName),
-			fmt.Sprintf("%s should%s match %s but it is%s", kase.BranchName, should, kase.Rule, infact),
+			"%s should%s match %s but it is%s", kase.BranchName, should, kase.Rule, infact,
 		)
 	}
 }
diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go
index c5bbfdedc2..d81f33f953 100644
--- a/models/issues/comment_test.go
+++ b/models/issues/comment_test.go
@@ -64,7 +64,7 @@ func TestFetchCodeComments(t *testing.T) {
 }
 
 func TestAsCommentType(t *testing.T) {
-	assert.Equal(t, issues_model.CommentType(0), issues_model.CommentTypeComment)
+	assert.Equal(t, issues_model.CommentTypeComment, issues_model.CommentType(0))
 	assert.Equal(t, issues_model.CommentTypeUndefined, issues_model.AsCommentType(""))
 	assert.Equal(t, issues_model.CommentTypeUndefined, issues_model.AsCommentType("nonsense"))
 	assert.Equal(t, issues_model.CommentTypeComment, issues_model.AsCommentType("comment"))
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 64fc20cc05..fe347c2715 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -125,8 +125,11 @@ type Issue struct {
 	IsPull            bool             `xorm:"INDEX"` // Indicates whether is a pull request or not.
 	PullRequest       *PullRequest     `xorm:"-"`
 	NumComments       int
-	Ref               string
-	PinOrder          int `xorm:"DEFAULT 0"`
+
+	// TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl"
+	Ref string
+
+	PinOrder int `xorm:"DEFAULT 0"`
 
 	DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
 
diff --git a/models/issues/issue_index.go b/models/issues/issue_index.go
index 16274d0ef0..2eb61858bf 100644
--- a/models/issues/issue_index.go
+++ b/models/issues/issue_index.go
@@ -18,12 +18,12 @@ func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error {
 	}
 	defer committer.Close()
 
-	var max int64
-	if _, err = db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil {
+	var maxIndex int64
+	if _, err = db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&maxIndex); err != nil {
 		return err
 	}
 
-	if err = db.SyncMaxResourceIndex(ctx, "issue_index", repoID, max); err != nil {
+	if err = db.SyncMaxResourceIndex(ctx, "issue_index", repoID, maxIndex); err != nil {
 		return err
 	}
 
diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go
index 548f137f39..dbbb1e4179 100644
--- a/models/issues/issue_test.go
+++ b/models/issues/issue_test.go
@@ -434,7 +434,7 @@ func assertCreateIssues(t *testing.T, isPull bool) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 	label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
 	milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1})
-	assert.EqualValues(t, milestone.ID, 1)
+	assert.EqualValues(t, 1, milestone.ID)
 	reaction := &issues_model.Reaction{
 		Type:   "heart",
 		UserID: owner.ID,
diff --git a/models/issues/issue_watch_test.go b/models/issues/issue_watch_test.go
index d4ce8d8d3d..fad94e243e 100644
--- a/models/issues/issue_watch_test.go
+++ b/models/issues/issue_watch_test.go
@@ -48,17 +48,17 @@ func TestGetIssueWatchers(t *testing.T) {
 	iws, err := issues_model.GetIssueWatchers(db.DefaultContext, 1, db.ListOptions{})
 	assert.NoError(t, err)
 	// Watcher is inactive, thus 0
-	assert.Len(t, iws, 0)
+	assert.Empty(t, iws)
 
 	iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 2, db.ListOptions{})
 	assert.NoError(t, err)
 	// Watcher is explicit not watching
-	assert.Len(t, iws, 0)
+	assert.Empty(t, iws)
 
 	iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 5, db.ListOptions{})
 	assert.NoError(t, err)
 	// Issue has no Watchers
-	assert.Len(t, iws, 0)
+	assert.Empty(t, iws)
 
 	iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 7, db.ListOptions{})
 	assert.NoError(t, err)
diff --git a/models/issues/label_test.go b/models/issues/label_test.go
index c2ff084c23..1d4b6f4684 100644
--- a/models/issues/label_test.go
+++ b/models/issues/label_test.go
@@ -31,12 +31,12 @@ func TestLabel_LoadSelectedLabelsAfterClick(t *testing.T) {
 	// First test : with negative and scope
 	label.LoadSelectedLabelsAfterClick([]int64{1, -8}, []string{"", "scope"})
 	assert.Equal(t, "1", label.QueryString)
-	assert.Equal(t, true, label.IsSelected)
+	assert.True(t, label.IsSelected)
 
 	// Second test : with duplicates
 	label.LoadSelectedLabelsAfterClick([]int64{1, 7, 1, 7, 7}, []string{"", "scope", "", "scope", "scope"})
 	assert.Equal(t, "1,8", label.QueryString)
-	assert.Equal(t, false, label.IsSelected)
+	assert.False(t, label.IsSelected)
 
 	// Third test : empty set
 	label.LoadSelectedLabelsAfterClick([]int64{}, []string{})
@@ -248,7 +248,7 @@ func TestGetLabelsByIssueID(t *testing.T) {
 
 	labels, err = issues_model.GetLabelsByIssueID(db.DefaultContext, unittest.NonexistentID)
 	assert.NoError(t, err)
-	assert.Len(t, labels, 0)
+	assert.Empty(t, labels)
 }
 
 func TestUpdateLabel(t *testing.T) {
@@ -271,7 +271,7 @@ func TestUpdateLabel(t *testing.T) {
 	assert.EqualValues(t, label.Color, newLabel.Color)
 	assert.EqualValues(t, label.Name, newLabel.Name)
 	assert.EqualValues(t, label.Description, newLabel.Description)
-	assert.EqualValues(t, newLabel.ArchivedUnix, 0)
+	assert.EqualValues(t, 0, newLabel.ArchivedUnix)
 	unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{})
 }
 
diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go
index e5f6f15ca2..28cd0c028b 100644
--- a/models/issues/milestone_test.go
+++ b/models/issues/milestone_test.go
@@ -87,7 +87,7 @@ func TestGetMilestonesByRepoID(t *testing.T) {
 		IsClosed: optional.Some(false),
 	})
 	assert.NoError(t, err)
-	assert.Len(t, milestones, 0)
+	assert.Empty(t, milestones)
 }
 
 func TestGetMilestones(t *testing.T) {
diff --git a/models/issues/pull_list_test.go b/models/issues/pull_list_test.go
index 8b814a0d0f..c7a898ca4e 100644
--- a/models/issues/pull_list_test.go
+++ b/models/issues/pull_list_test.go
@@ -40,7 +40,7 @@ func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, reviewComments, 2)
 	for _, pr := range prs {
-		assert.EqualValues(t, reviewComments[pr.IssueID], 1)
+		assert.EqualValues(t, 1, reviewComments[pr.IssueID])
 	}
 }
 
diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go
index cb7b47263d..090659864a 100644
--- a/models/issues/pull_test.go
+++ b/models/issues/pull_test.go
@@ -83,7 +83,7 @@ func TestLoadRequestedReviewers(t *testing.T) {
 	assert.NoError(t, pull.LoadIssue(db.DefaultContext))
 	issue := pull.Issue
 	assert.NoError(t, issue.LoadRepo(db.DefaultContext))
-	assert.Len(t, pull.RequestedReviewers, 0)
+	assert.Empty(t, pull.RequestedReviewers)
 
 	user1, err := user_model.GetUserByID(db.DefaultContext, 1)
 	assert.NoError(t, err)
diff --git a/models/issues/stopwatch_test.go b/models/issues/stopwatch_test.go
index 39958a7f36..a1bf9dc931 100644
--- a/models/issues/stopwatch_test.go
+++ b/models/issues/stopwatch_test.go
@@ -32,7 +32,7 @@ func TestCancelStopwatch(t *testing.T) {
 
 	_ = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID})
 
-	assert.Nil(t, issues_model.CancelStopwatch(db.DefaultContext, user1, issue2))
+	assert.NoError(t, issues_model.CancelStopwatch(db.DefaultContext, user1, issue2))
 }
 
 func TestStopwatchExists(t *testing.T) {
diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go
index d82bff967a..44054a1b83 100644
--- a/models/issues/tracked_time_test.go
+++ b/models/issues/tracked_time_test.go
@@ -50,7 +50,7 @@ func TestGetTrackedTimes(t *testing.T) {
 
 	times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: -1})
 	assert.NoError(t, err)
-	assert.Len(t, times, 0)
+	assert.Empty(t, times)
 
 	// by User
 	times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 1})
@@ -60,7 +60,7 @@ func TestGetTrackedTimes(t *testing.T) {
 
 	times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 3})
 	assert.NoError(t, err)
-	assert.Len(t, times, 0)
+	assert.Empty(t, times)
 
 	// by Repo
 	times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 2})
@@ -69,7 +69,7 @@ func TestGetTrackedTimes(t *testing.T) {
 	assert.Equal(t, int64(1), times[0].Time)
 	issue, err := issues_model.GetIssueByID(db.DefaultContext, times[0].IssueID)
 	assert.NoError(t, err)
-	assert.Equal(t, issue.RepoID, int64(2))
+	assert.Equal(t, int64(2), issue.RepoID)
 
 	times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 1})
 	assert.NoError(t, err)
@@ -77,7 +77,7 @@ func TestGetTrackedTimes(t *testing.T) {
 
 	times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 10})
 	assert.NoError(t, err)
-	assert.Len(t, times, 0)
+	assert.Empty(t, times)
 }
 
 func TestTotalTimesForEachUser(t *testing.T) {
diff --git a/models/migrations/v1_16/v193_test.go b/models/migrations/v1_16/v193_test.go
index d99bbc2962..b279967a2c 100644
--- a/models/migrations/v1_16/v193_test.go
+++ b/models/migrations/v1_16/v193_test.go
@@ -56,8 +56,8 @@ func Test_AddRepoIDForAttachment(t *testing.T) {
 	err := x.Table("attachment").Where("issue_id > 0").Find(&issueAttachments)
 	assert.NoError(t, err)
 	for _, attach := range issueAttachments {
-		assert.Greater(t, attach.RepoID, int64(0))
-		assert.Greater(t, attach.IssueID, int64(0))
+		assert.Positive(t, attach.RepoID)
+		assert.Positive(t, attach.IssueID)
 		var issue Issue
 		has, err := x.ID(attach.IssueID).Get(&issue)
 		assert.NoError(t, err)
@@ -69,8 +69,8 @@ func Test_AddRepoIDForAttachment(t *testing.T) {
 	err = x.Table("attachment").Where("release_id > 0").Find(&releaseAttachments)
 	assert.NoError(t, err)
 	for _, attach := range releaseAttachments {
-		assert.Greater(t, attach.RepoID, int64(0))
-		assert.Greater(t, attach.ReleaseID, int64(0))
+		assert.Positive(t, attach.RepoID)
+		assert.Positive(t, attach.ReleaseID)
 		var release Release
 		has, err := x.ID(attach.ReleaseID).Get(&release)
 		assert.NoError(t, err)
diff --git a/models/migrations/v1_22/v286_test.go b/models/migrations/v1_22/v286_test.go
index a19c9396e2..1f213ddb6e 100644
--- a/models/migrations/v1_22/v286_test.go
+++ b/models/migrations/v1_22/v286_test.go
@@ -107,12 +107,12 @@ func Test_RepositoryFormat(t *testing.T) {
 	repo = new(Repository)
 	ok, err := x.ID(2).Get(repo)
 	assert.NoError(t, err)
-	assert.EqualValues(t, true, ok)
+	assert.True(t, ok)
 	assert.EqualValues(t, "sha1", repo.ObjectFormatName)
 
 	repo = new(Repository)
 	ok, err = x.ID(id).Get(repo)
 	assert.NoError(t, err)
-	assert.EqualValues(t, true, ok)
+	assert.True(t, ok)
 	assert.EqualValues(t, "sha256", repo.ObjectFormatName)
 }
diff --git a/models/migrations/v1_22/v294_test.go b/models/migrations/v1_22/v294_test.go
index 82a3bcd602..a1d702cb77 100644
--- a/models/migrations/v1_22/v294_test.go
+++ b/models/migrations/v1_22/v294_test.go
@@ -39,7 +39,7 @@ func Test_AddUniqueIndexForProjectIssue(t *testing.T) {
 
 	tables, err := x.DBMetas()
 	assert.NoError(t, err)
-	assert.EqualValues(t, 1, len(tables))
+	assert.Len(t, tables, 1)
 	found := false
 	for _, index := range tables[0].Indexes {
 		if index.Type == schemas.UniqueType {
diff --git a/models/organization/org_list_test.go b/models/organization/org_list_test.go
index edc8996f3e..0f0f8a4bcd 100644
--- a/models/organization/org_list_test.go
+++ b/models/organization/org_list_test.go
@@ -40,7 +40,7 @@ func TestFindOrgs(t *testing.T) {
 		IncludePrivate: false,
 	})
 	assert.NoError(t, err)
-	assert.Len(t, orgs, 0)
+	assert.Empty(t, orgs)
 
 	total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{
 		UserID:         4,
diff --git a/models/organization/org_test.go b/models/organization/org_test.go
index 7159f0fc46..5e99e88689 100644
--- a/models/organization/org_test.go
+++ b/models/organization/org_test.go
@@ -283,7 +283,7 @@ func TestGetOrgUsersByOrgID(t *testing.T) {
 		OrgID:       unittest.NonexistentID,
 	})
 	assert.NoError(t, err)
-	assert.Len(t, orgUsers, 0)
+	assert.Empty(t, orgUsers)
 }
 
 func TestChangeOrgUserStatus(t *testing.T) {
diff --git a/models/perm/access_mode_test.go b/models/perm/access_mode_test.go
index 982fceee5a..c4c7d483fb 100644
--- a/models/perm/access_mode_test.go
+++ b/models/perm/access_mode_test.go
@@ -15,7 +15,7 @@ func TestAccessMode(t *testing.T) {
 		m := ParseAccessMode(name)
 		assert.Equal(t, AccessMode(i), m)
 	}
-	assert.Equal(t, AccessMode(4), AccessModeOwner)
+	assert.Equal(t, AccessModeOwner, AccessMode(4))
 	assert.Equal(t, "owner", AccessModeOwner.ToString())
 	assert.Equal(t, AccessModeNone, ParseAccessMode("owner"))
 	assert.Equal(t, AccessModeNone, ParseAccessMode("invalid"))
diff --git a/models/project/column_test.go b/models/project/column_test.go
index 911649fb72..566667e45d 100644
--- a/models/project/column_test.go
+++ b/models/project/column_test.go
@@ -5,7 +5,6 @@ package project
 
 import (
 	"fmt"
-	"strings"
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
@@ -66,7 +65,7 @@ func Test_moveIssuesToAnotherColumn(t *testing.T) {
 
 	issues, err = column1.GetIssues(db.DefaultContext)
 	assert.NoError(t, err)
-	assert.Len(t, issues, 0)
+	assert.Empty(t, issues)
 
 	issues, err = column2.GetIssues(db.DefaultContext)
 	assert.NoError(t, err)
@@ -123,5 +122,5 @@ func Test_NewColumn(t *testing.T) {
 		ProjectID: project1.ID,
 	})
 	assert.Error(t, err)
-	assert.True(t, strings.Contains(err.Error(), "maximum number of columns reached"))
+	assert.Contains(t, err.Error(), "maximum number of columns reached")
 }
diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go
index 6468e0f605..6d88d170da 100644
--- a/models/repo/repo_test.go
+++ b/models/repo/repo_test.go
@@ -144,8 +144,8 @@ func TestGetRepositoryByURL(t *testing.T) {
 			assert.NotNil(t, repo)
 			assert.NoError(t, err)
 
-			assert.Equal(t, repo.ID, int64(2))
-			assert.Equal(t, repo.OwnerID, int64(2))
+			assert.Equal(t, int64(2), repo.ID)
+			assert.Equal(t, int64(2), repo.OwnerID)
 		}
 
 		test(t, "https://try.gitea.io/user2/repo2")
@@ -159,8 +159,8 @@ func TestGetRepositoryByURL(t *testing.T) {
 			assert.NotNil(t, repo)
 			assert.NoError(t, err)
 
-			assert.Equal(t, repo.ID, int64(2))
-			assert.Equal(t, repo.OwnerID, int64(2))
+			assert.Equal(t, int64(2), repo.ID)
+			assert.Equal(t, int64(2), repo.OwnerID)
 		}
 
 		test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2")
@@ -177,8 +177,8 @@ func TestGetRepositoryByURL(t *testing.T) {
 			assert.NotNil(t, repo)
 			assert.NoError(t, err)
 
-			assert.Equal(t, repo.ID, int64(2))
-			assert.Equal(t, repo.OwnerID, int64(2))
+			assert.Equal(t, int64(2), repo.ID)
+			assert.Equal(t, int64(2), repo.OwnerID)
 		}
 
 		test(t, "sshuser@try.gitea.io:user2/repo2")
diff --git a/models/repo/star_test.go b/models/repo/star_test.go
index aaac89d975..b540f54310 100644
--- a/models/repo/star_test.go
+++ b/models/repo/star_test.go
@@ -52,7 +52,7 @@ func TestRepository_GetStargazers2(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
 	gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0})
 	assert.NoError(t, err)
-	assert.Len(t, gazers, 0)
+	assert.Empty(t, gazers)
 }
 
 func TestClearRepoStars(t *testing.T) {
@@ -71,5 +71,5 @@ func TestClearRepoStars(t *testing.T) {
 
 	gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0})
 	assert.NoError(t, err)
-	assert.Len(t, gazers, 0)
+	assert.Empty(t, gazers)
 }
diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go
index f2abc2ffa0..44ebe5f214 100644
--- a/models/repo/user_repo_test.go
+++ b/models/repo/user_repo_test.go
@@ -21,7 +21,7 @@ func TestRepoAssignees(t *testing.T) {
 	users, err := repo_model.GetRepoAssignees(db.DefaultContext, repo2)
 	assert.NoError(t, err)
 	assert.Len(t, users, 1)
-	assert.Equal(t, users[0].ID, int64(2))
+	assert.Equal(t, int64(2), users[0].ID)
 
 	repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21})
 	users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go
index a95a267961..c39ef607e8 100644
--- a/models/repo/watch_test.go
+++ b/models/repo/watch_test.go
@@ -41,7 +41,7 @@ func TestGetWatchers(t *testing.T) {
 
 	watches, err = repo_model.GetWatchers(db.DefaultContext, unittest.NonexistentID)
 	assert.NoError(t, err)
-	assert.Len(t, watches, 0)
+	assert.Empty(t, watches)
 }
 
 func TestRepository_GetWatchers(t *testing.T) {
@@ -58,7 +58,7 @@ func TestRepository_GetWatchers(t *testing.T) {
 	repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 9})
 	watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1})
 	assert.NoError(t, err)
-	assert.Len(t, watchers, 0)
+	assert.Empty(t, watchers)
 }
 
 func TestWatchIfAuto(t *testing.T) {
diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go
index 3b0f28d3a6..4ac858e04e 100644
--- a/models/unittest/unit_tests.go
+++ b/models/unittest/unit_tests.go
@@ -79,7 +79,7 @@ func AssertExistsAndLoadMap(t assert.TestingT, table string, conditions ...any)
 	e := db.GetEngine(db.DefaultContext).Table(table)
 	res, err := whereOrderConditions(e, conditions).Query()
 	assert.NoError(t, err)
-	assert.True(t, len(res) == 1,
+	assert.Len(t, res, 1,
 		"Expected to find one row in %s (with conditions %+v), but found %d",
 		table, conditions, len(res),
 	)
diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go
index c2e010d95b..d72d873de2 100644
--- a/models/user/email_address_test.go
+++ b/models/user/email_address_test.go
@@ -97,8 +97,7 @@ func TestListEmails(t *testing.T) {
 	}
 	emails, count, err := user_model.SearchEmails(db.DefaultContext, opts)
 	assert.NoError(t, err)
-	assert.NotEqual(t, int64(0), count)
-	assert.True(t, count > 5)
+	assert.Greater(t, count, int64(5))
 
 	contains := func(match func(s *user_model.SearchEmailResult) bool) bool {
 		for _, v := range emails {
diff --git a/models/user/setting_test.go b/models/user/setting_test.go
index c56fe93075..c607d9fd00 100644
--- a/models/user/setting_test.go
+++ b/models/user/setting_test.go
@@ -56,5 +56,5 @@ func TestSettings(t *testing.T) {
 	assert.NoError(t, err)
 	settings, err = user_model.GetUserAllSettings(db.DefaultContext, 99)
 	assert.NoError(t, err)
-	assert.Len(t, settings, 0)
+	assert.Empty(t, settings)
 }
diff --git a/models/user/user_test.go b/models/user/user_test.go
index 6701be39a5..7ebc64f69e 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -201,7 +201,7 @@ func TestNewGitSig(t *testing.T) {
 		assert.NotContains(t, sig.Name, "<")
 		assert.NotContains(t, sig.Name, ">")
 		assert.NotContains(t, sig.Name, "\n")
-		assert.NotEqual(t, len(strings.TrimSpace(sig.Name)), 0)
+		assert.NotEmpty(t, strings.TrimSpace(sig.Name))
 	}
 }
 
@@ -216,7 +216,7 @@ func TestDisplayName(t *testing.T) {
 		if len(strings.TrimSpace(user.FullName)) == 0 {
 			assert.Equal(t, user.Name, displayName)
 		}
-		assert.NotEqual(t, len(strings.TrimSpace(displayName)), 0)
+		assert.NotEmpty(t, strings.TrimSpace(displayName))
 	}
 }
 
@@ -322,15 +322,15 @@ func TestGetMaileableUsersByIDs(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, results, 1)
 	if len(results) > 1 {
-		assert.Equal(t, results[0].ID, 1)
+		assert.Equal(t, 1, results[0].ID)
 	}
 
 	results, err = user_model.GetMaileableUsersByIDs(db.DefaultContext, []int64{1, 4}, true)
 	assert.NoError(t, err)
 	assert.Len(t, results, 2)
 	if len(results) > 2 {
-		assert.Equal(t, results[0].ID, 1)
-		assert.Equal(t, results[1].ID, 4)
+		assert.Equal(t, 1, results[0].ID)
+		assert.Equal(t, 4, results[1].ID)
 	}
 }
 
@@ -499,7 +499,7 @@ func Test_ValidateUser(t *testing.T) {
 		{ID: 2, Visibility: structs.VisibleTypePrivate}: true,
 	}
 	for kase, expected := range kases {
-		assert.EqualValues(t, expected, nil == user_model.ValidateUser(kase), fmt.Sprintf("case: %+v", kase))
+		assert.EqualValues(t, expected, nil == user_model.ValidateUser(kase), "case: %+v", kase)
 	}
 }
 
@@ -570,11 +570,11 @@ func TestDisabledUserFeatures(t *testing.T) {
 
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 
-	assert.Len(t, setting.Admin.UserDisabledFeatures.Values(), 0)
+	assert.Empty(t, setting.Admin.UserDisabledFeatures.Values())
 
 	// no features should be disabled with a plain login type
 	assert.LessOrEqual(t, user.LoginType, auth.Plain)
-	assert.Len(t, user_model.DisabledFeaturesWithLoginType(user).Values(), 0)
+	assert.Empty(t, user_model.DisabledFeaturesWithLoginType(user).Values())
 	for _, f := range testValues.Values() {
 		assert.False(t, user_model.IsFeatureDisabledWithLoginType(user, f))
 	}
@@ -600,5 +600,5 @@ func TestGetInactiveUsers(t *testing.T) {
 	interval := time.Now().Unix() - 1730468968 + 3600*24
 	users, err = user_model.GetInactiveUsers(db.DefaultContext, time.Duration(interval*int64(time.Second)))
 	assert.NoError(t, err)
-	assert.Len(t, users, 0)
+	assert.Empty(t, users)
 }
diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go
index f4403776ce..c6c3f40d46 100644
--- a/models/webhook/webhook_test.go
+++ b/models/webhook/webhook_test.go
@@ -43,7 +43,7 @@ func TestWebhook_History(t *testing.T) {
 	webhook = unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2})
 	tasks, err = webhook.History(db.DefaultContext, 0)
 	assert.NoError(t, err)
-	assert.Len(t, tasks, 0)
+	assert.Empty(t, tasks)
 }
 
 func TestWebhook_UpdateEvent(t *testing.T) {
@@ -206,7 +206,7 @@ func TestHookTasks(t *testing.T) {
 
 	hookTasks, err = HookTasks(db.DefaultContext, unittest.NonexistentID, 1)
 	assert.NoError(t, err)
-	assert.Len(t, hookTasks, 0)
+	assert.Empty(t, hookTasks)
 }
 
 func TestCreateHookTask(t *testing.T) {
diff --git a/modules/activitypub/client_test.go b/modules/activitypub/client_test.go
index 65ea8d4d5b..d0c4845445 100644
--- a/modules/activitypub/client_test.go
+++ b/modules/activitypub/client_test.go
@@ -8,7 +8,6 @@ import (
 	"io"
 	"net/http"
 	"net/http/httptest"
-	"regexp"
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
@@ -28,9 +27,9 @@ func TestActivityPubSignedPost(t *testing.T) {
 
 	expected := "BODY"
 	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		assert.Regexp(t, regexp.MustCompile("^"+setting.Federation.DigestAlgorithm), r.Header.Get("Digest"))
+		assert.Regexp(t, "^"+setting.Federation.DigestAlgorithm, r.Header.Get("Digest"))
 		assert.Contains(t, r.Header.Get("Signature"), pubID)
-		assert.Equal(t, r.Header.Get("Content-Type"), ActivityStreamsContentType)
+		assert.Equal(t, ActivityStreamsContentType, r.Header.Get("Content-Type"))
 		body, err := io.ReadAll(r.Body)
 		assert.NoError(t, err)
 		assert.Equal(t, expected, string(body))
diff --git a/modules/assetfs/layered_test.go b/modules/assetfs/layered_test.go
index b82111e745..03a3ae0d7c 100644
--- a/modules/assetfs/layered_test.go
+++ b/modules/assetfs/layered_test.go
@@ -58,7 +58,7 @@ func TestLayered(t *testing.T) {
 	assertRead := func(expected string, expectedErr error, elems ...string) {
 		bs, err := assets.ReadFile(elems...)
 		if err != nil {
-			assert.ErrorAs(t, err, &expectedErr)
+			assert.ErrorIs(t, err, expectedErr)
 		} else {
 			assert.NoError(t, err)
 			assert.Equal(t, expected, string(bs))
diff --git a/modules/auth/pam/pam_test.go b/modules/auth/pam/pam_test.go
index c277d59c41..7265b5d0c1 100644
--- a/modules/auth/pam/pam_test.go
+++ b/modules/auth/pam/pam_test.go
@@ -15,5 +15,5 @@ func TestPamAuth(t *testing.T) {
 	result, err := Auth("gitea", "user1", "false-pwd")
 	assert.Error(t, err)
 	assert.EqualError(t, err, "Authentication failure")
-	assert.Len(t, result, 0)
+	assert.Len(t, result)
 }
diff --git a/modules/auth/password/hash/dummy_test.go b/modules/auth/password/hash/dummy_test.go
index f3b36df625..e56e3f1a7f 100644
--- a/modules/auth/password/hash/dummy_test.go
+++ b/modules/auth/password/hash/dummy_test.go
@@ -18,7 +18,7 @@ func TestDummyHasher(t *testing.T) {
 	password, salt := "password", "ZogKvWdyEx"
 
 	hash, err := dummy.Hash(password, salt)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	assert.Equal(t, hash, salt+":"+password)
 
 	assert.True(t, dummy.VerifyPassword(password, hash, salt))
diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go
index 85f9780709..c66b62937f 100644
--- a/modules/auth/password/password.go
+++ b/modules/auth/password/password.go
@@ -99,10 +99,10 @@ func IsComplexEnough(pwd string) bool {
 func Generate(n int) (string, error) {
 	NewComplexity()
 	buffer := make([]byte, n)
-	max := big.NewInt(int64(len(validChars)))
+	maxInt := big.NewInt(int64(len(validChars)))
 	for {
 		for j := 0; j < n; j++ {
-			rnd, err := rand.Int(rand.Reader, max)
+			rnd, err := rand.Int(rand.Reader, maxInt)
 			if err != nil {
 				return "", err
 			}
diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go
index 86cccdf209..f63679048e 100644
--- a/modules/base/tool_test.go
+++ b/modules/base/tool_test.go
@@ -6,7 +6,6 @@ package base
 import (
 	"crypto/sha1"
 	"fmt"
-	"os"
 	"testing"
 	"time"
 
@@ -157,7 +156,7 @@ func TestStringsToInt64s(t *testing.T) {
 	testSuccess([]string{"1", "4", "16", "64", "256"}, []int64{1, 4, 16, 64, 256})
 
 	ints, err := StringsToInt64s([]string{"-1", "a"})
-	assert.Len(t, ints, 0)
+	assert.Empty(t, ints)
 	assert.Error(t, err)
 }
 
@@ -172,9 +171,9 @@ func TestInt64sToStrings(t *testing.T) {
 // TODO: Test EntryIcon
 
 func TestSetupGiteaRoot(t *testing.T) {
-	_ = os.Setenv("GITEA_ROOT", "test")
+	t.Setenv("GITEA_ROOT", "test")
 	assert.Equal(t, "test", SetupGiteaRoot())
-	_ = os.Setenv("GITEA_ROOT", "")
+	t.Setenv("GITEA_ROOT", "")
 	assert.NotEqual(t, "test", SetupGiteaRoot())
 }
 
diff --git a/modules/dump/dumper_test.go b/modules/dump/dumper_test.go
index b444fa2de5..2db3a598a4 100644
--- a/modules/dump/dumper_test.go
+++ b/modules/dump/dumper_test.go
@@ -25,7 +25,7 @@ func TestPrepareFileNameAndType(t *testing.T) {
 		assert.Equal(t,
 			fmt.Sprintf("outFile=%s, outType=%s", expFile, expType),
 			fmt.Sprintf("outFile=%s, outType=%s", outFile, outType),
-			fmt.Sprintf("argFile=%s, argType=%s", argFile, argType),
+			"argFile=%s, argType=%s", argFile, argType,
 		)
 	}
 
diff --git a/modules/git/commit_sha256_test.go b/modules/git/commit_sha256_test.go
index 3b8b6d3763..2184a9c47c 100644
--- a/modules/git/commit_sha256_test.go
+++ b/modules/git/commit_sha256_test.go
@@ -146,7 +146,7 @@ func TestHasPreviousCommitSha256(t *testing.T) {
 	parentSHA := MustIDFromString("b0ec7af4547047f12d5093e37ef8f1b3b5415ed8ee17894d43a34d7d34212e9c")
 	notParentSHA := MustIDFromString("42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236")
 	assert.Equal(t, objectFormat, parentSHA.Type())
-	assert.Equal(t, objectFormat.Name(), "sha256")
+	assert.Equal(t, "sha256", objectFormat.Name())
 
 	haz, err := commit.HasPreviousCommit(parentSHA)
 	assert.NoError(t, err)
diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go
index bf381a5350..6ac65564dc 100644
--- a/modules/git/commit_test.go
+++ b/modules/git/commit_test.go
@@ -343,9 +343,9 @@ func TestGetCommitFileStatusMerges(t *testing.T) {
 		},
 	}
 
-	assert.Equal(t, commitFileStatus.Added, expected.Added)
-	assert.Equal(t, commitFileStatus.Removed, expected.Removed)
-	assert.Equal(t, commitFileStatus.Modified, expected.Modified)
+	assert.Equal(t, expected.Added, commitFileStatus.Added)
+	assert.Equal(t, expected.Removed, commitFileStatus.Removed)
+	assert.Equal(t, expected.Modified, commitFileStatus.Modified)
 }
 
 func Test_GetCommitBranchStart(t *testing.T) {
diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go
index 6a99f80407..005d539726 100644
--- a/modules/git/grep_test.go
+++ b/modules/git/grep_test.go
@@ -73,9 +73,9 @@ func TestGrepSearch(t *testing.T) {
 
 	res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{})
 	assert.NoError(t, err)
-	assert.Len(t, res, 0)
+	assert.Empty(t, res)
 
 	res, err = GrepSearch(context.Background(), &Repository{Path: "no-such-git-repo"}, "no-such-content", GrepOptions{})
 	assert.Error(t, err)
-	assert.Len(t, res, 0)
+	assert.Empty(t, res)
 }
diff --git a/modules/git/parse_nogogit_test.go b/modules/git/parse_nogogit_test.go
index 23fddb014c..a4436ce499 100644
--- a/modules/git/parse_nogogit_test.go
+++ b/modules/git/parse_nogogit_test.go
@@ -100,5 +100,5 @@ func TestParseTreeEntriesInvalid(t *testing.T) {
 	// there was a panic: "runtime error: slice bounds out of range" when the input was invalid: #20315
 	entries, err := ParseTreeEntries([]byte("100644 blob ea0d83c9081af9500ac9f804101b3fd0a5c293af"))
 	assert.Error(t, err)
-	assert.Len(t, entries, 0)
+	assert.Empty(t, entries)
 }
diff --git a/modules/git/repo_branch_test.go b/modules/git/repo_branch_test.go
index 009c545832..5d3b8abb3a 100644
--- a/modules/git/repo_branch_test.go
+++ b/modules/git/repo_branch_test.go
@@ -34,7 +34,7 @@ func TestRepository_GetBranches(t *testing.T) {
 	branches, countAll, err = bareRepo1.GetBranchNames(5, 1)
 
 	assert.NoError(t, err)
-	assert.Len(t, branches, 0)
+	assert.Empty(t, branches)
 	assert.EqualValues(t, 3, countAll)
 	assert.ElementsMatch(t, []string{}, branches)
 }
@@ -66,7 +66,7 @@ func TestGetRefsBySha(t *testing.T) {
 	// do not exist
 	branches, err := bareRepo5.GetRefsBySha("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", "")
 	assert.NoError(t, err)
-	assert.Len(t, branches, 0)
+	assert.Empty(t, branches)
 
 	// refs/pull/1/head
 	branches, err = bareRepo5.GetRefsBySha("c83380d7056593c51a699d12b9c00627bd5743e9", PullPrefix)
diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go
index 9405634df1..9ffadb833d 100644
--- a/modules/git/repo_commit.go
+++ b/modules/git/repo_commit.go
@@ -465,15 +465,15 @@ func (repo *Repository) getBranches(env []string, commitID string, limit int) ([
 
 	refs := strings.Split(stdout, "\n")
 
-	var max int
+	var maxNum int
 	if len(refs) > limit {
-		max = limit
+		maxNum = limit
 	} else {
-		max = len(refs) - 1
+		maxNum = len(refs) - 1
 	}
 
-	branches := make([]string, max)
-	for i, ref := range refs[:max] {
+	branches := make([]string, maxNum)
+	for i, ref := range refs[:maxNum] {
 		parts := strings.Fields(ref)
 
 		branches[i] = parts[len(parts)-1]
diff --git a/modules/git/repo_compare_test.go b/modules/git/repo_compare_test.go
index d4597bd948..9f8302a55f 100644
--- a/modules/git/repo_compare_test.go
+++ b/modules/git/repo_compare_test.go
@@ -97,7 +97,7 @@ func TestReadPatch(t *testing.T) {
 	assert.Empty(t, noFile)
 	assert.Empty(t, noCommit)
 	assert.Len(t, oldCommit, 40)
-	assert.True(t, oldCommit == "6e8e2a6f9efd71dbe6917816343ed8415ad696c3")
+	assert.Equal(t, "6e8e2a6f9efd71dbe6917816343ed8415ad696c3", oldCommit)
 }
 
 func TestReadWritePullHead(t *testing.T) {
@@ -138,7 +138,7 @@ func TestReadWritePullHead(t *testing.T) {
 	}
 
 	assert.Len(t, headContents, 40)
-	assert.True(t, headContents == newCommit)
+	assert.Equal(t, headContents, newCommit)
 
 	// Remove file after the test
 	err = repo.RemoveReference(PullPrefix + "1/head")
diff --git a/modules/graceful/manager.go b/modules/graceful/manager.go
index 3f1115066a..991b2f2b7a 100644
--- a/modules/graceful/manager.go
+++ b/modules/graceful/manager.go
@@ -218,13 +218,13 @@ func (g *Manager) ServerDone() {
 	g.runningServerWaitGroup.Done()
 }
 
-func (g *Manager) setStateTransition(old, new state) bool {
+func (g *Manager) setStateTransition(oldState, newState state) bool {
 	g.lock.Lock()
-	if g.state != old {
+	if g.state != oldState {
 		g.lock.Unlock()
 		return false
 	}
-	g.state = new
+	g.state = newState
 	g.lock.Unlock()
 	return true
 }
diff --git a/modules/indexer/internal/bleve/query.go b/modules/indexer/internal/bleve/query.go
index 21422b281c..1b18ca1a77 100644
--- a/modules/indexer/internal/bleve/query.go
+++ b/modules/indexer/internal/bleve/query.go
@@ -35,18 +35,18 @@ func BoolFieldQuery(value bool, field string) *query.BoolFieldQuery {
 	return q
 }
 
-func NumericRangeInclusiveQuery(min, max optional.Option[int64], field string) *query.NumericRangeQuery {
+func NumericRangeInclusiveQuery(minOption, maxOption optional.Option[int64], field string) *query.NumericRangeQuery {
 	var minF, maxF *float64
 	var minI, maxI *bool
-	if min.Has() {
+	if minOption.Has() {
 		minF = new(float64)
-		*minF = float64(min.Value())
+		*minF = float64(minOption.Value())
 		minI = new(bool)
 		*minI = true
 	}
-	if max.Has() {
+	if maxOption.Has() {
 		maxF = new(float64)
-		*maxF = float64(max.Value())
+		*maxF = float64(maxOption.Value())
 		maxI = new(bool)
 		*maxI = true
 	}
diff --git a/modules/indexer/internal/paginator.go b/modules/indexer/internal/paginator.go
index ee204bf047..f1e19740eb 100644
--- a/modules/indexer/internal/paginator.go
+++ b/modules/indexer/internal/paginator.go
@@ -10,12 +10,12 @@ import (
 )
 
 // ParsePaginator parses a db.Paginator into a skip and limit
-func ParsePaginator(paginator *db.ListOptions, max ...int) (int, int) {
+func ParsePaginator(paginator *db.ListOptions, maxNums ...int) (int, int) {
 	// Use a very large number to indicate no limit
 	unlimited := math.MaxInt32
-	if len(max) > 0 {
+	if len(maxNums) > 0 {
 		// Some indexer engines have a limit on the page size, respect that
-		unlimited = max[0]
+		unlimited = maxNums[0]
 	}
 
 	if paginator == nil || paginator.IsListAll() {
diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go
index 16f0a78ec0..94ce8520bf 100644
--- a/modules/indexer/issues/internal/tests/tests.go
+++ b/modules/indexer/issues/internal/tests/tests.go
@@ -113,7 +113,7 @@ var cases = []*testIndexerCase{
 			},
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			assert.Equal(t, len(data), int(result.Total))
 		},
 	},
@@ -176,7 +176,7 @@ var cases = []*testIndexerCase{
 			IsPull: optional.Some(false),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.False(t, data[v.ID].IsPull)
 			}
@@ -192,7 +192,7 @@ var cases = []*testIndexerCase{
 			IsPull: optional.Some(true),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.True(t, data[v.ID].IsPull)
 			}
@@ -208,7 +208,7 @@ var cases = []*testIndexerCase{
 			IsClosed: optional.Some(false),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.False(t, data[v.ID].IsClosed)
 			}
@@ -224,7 +224,7 @@ var cases = []*testIndexerCase{
 			IsClosed: optional.Some(true),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.True(t, data[v.ID].IsClosed)
 			}
@@ -274,7 +274,7 @@ var cases = []*testIndexerCase{
 			MilestoneIDs: []int64{1, 2, 6},
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.Contains(t, []int64{1, 2, 6}, data[v.ID].MilestoneID)
 			}
@@ -292,7 +292,7 @@ var cases = []*testIndexerCase{
 			MilestoneIDs: []int64{0},
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.Equal(t, int64(0), data[v.ID].MilestoneID)
 			}
@@ -310,7 +310,7 @@ var cases = []*testIndexerCase{
 			ProjectID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.Equal(t, int64(1), data[v.ID].ProjectID)
 			}
@@ -328,7 +328,7 @@ var cases = []*testIndexerCase{
 			ProjectID: optional.Some(int64(0)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.Equal(t, int64(0), data[v.ID].ProjectID)
 			}
@@ -346,7 +346,7 @@ var cases = []*testIndexerCase{
 			ProjectColumnID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.Equal(t, int64(1), data[v.ID].ProjectColumnID)
 			}
@@ -364,7 +364,7 @@ var cases = []*testIndexerCase{
 			ProjectColumnID: optional.Some(int64(0)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.Equal(t, int64(0), data[v.ID].ProjectColumnID)
 			}
@@ -382,7 +382,7 @@ var cases = []*testIndexerCase{
 			PosterID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.Equal(t, int64(1), data[v.ID].PosterID)
 			}
@@ -400,7 +400,7 @@ var cases = []*testIndexerCase{
 			AssigneeID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.Equal(t, int64(1), data[v.ID].AssigneeID)
 			}
@@ -418,7 +418,7 @@ var cases = []*testIndexerCase{
 			AssigneeID: optional.Some(int64(0)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.Equal(t, int64(0), data[v.ID].AssigneeID)
 			}
@@ -436,7 +436,7 @@ var cases = []*testIndexerCase{
 			MentionID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.Contains(t, data[v.ID].MentionIDs, int64(1))
 			}
@@ -454,7 +454,7 @@ var cases = []*testIndexerCase{
 			ReviewedID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.Contains(t, data[v.ID].ReviewedIDs, int64(1))
 			}
@@ -472,7 +472,7 @@ var cases = []*testIndexerCase{
 			ReviewRequestedID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.Contains(t, data[v.ID].ReviewRequestedIDs, int64(1))
 			}
@@ -490,7 +490,7 @@ var cases = []*testIndexerCase{
 			SubscriberID: optional.Some(int64(1)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.Contains(t, data[v.ID].SubscriberIDs, int64(1))
 			}
@@ -509,7 +509,7 @@ var cases = []*testIndexerCase{
 			UpdatedBeforeUnix: optional.Some(int64(30)),
 		},
 		Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
-			assert.Equal(t, 5, len(result.Hits))
+			assert.Len(t, result.Hits, 5)
 			for _, v := range result.Hits {
 				assert.GreaterOrEqual(t, data[v.ID].UpdatedUnix, int64(20))
 				assert.LessOrEqual(t, data[v.ID].UpdatedUnix, int64(30))
diff --git a/modules/lfs/http_client.go b/modules/lfs/http_client.go
index 50f0e7a8d8..3acd23b8f7 100644
--- a/modules/lfs/http_client.go
+++ b/modules/lfs/http_client.go
@@ -72,7 +72,10 @@ func (c *HTTPClient) batch(ctx context.Context, operation string, objects []Poin
 
 	url := fmt.Sprintf("%s/objects/batch", c.endpoint)
 
-	request := &BatchRequest{operation, c.transferNames(), nil, objects}
+	// `ref` is an "optional object describing the server ref that the objects belong to"
+	// but some (incorrect) lfs servers require it, so maybe adding an empty ref here doesn't break the correct ones.
+	// https://github.com/git-lfs/git-lfs/blob/a32a02b44bf8a511aa14f047627c49e1a7fd5021/docs/api/batch.md?plain=1#L37
+	request := &BatchRequest{operation, c.transferNames(), &Reference{}, objects}
 	payload := new(bytes.Buffer)
 	err := json.NewEncoder(payload).Encode(request)
 	if err != nil {
diff --git a/modules/lfs/shared.go b/modules/lfs/shared.go
index 40ad789c1d..cd9488e3db 100644
--- a/modules/lfs/shared.go
+++ b/modules/lfs/shared.go
@@ -14,9 +14,12 @@ import (
 const (
 	// MediaType contains the media type for LFS server requests
 	MediaType = "application/vnd.git-lfs+json"
-	// Some LFS servers offer content with other types, so fallback to '*/*' if application/vnd.git-lfs+json cannot be served
-	AcceptHeader    = "application/vnd.git-lfs+json;q=0.9, */*;q=0.8"
-	UserAgentHeader = "git-lfs"
+	// AcceptHeader Some LFS servers offer content with other types, so fallback to '*/*' if application/vnd.git-lfs+json cannot be served
+	AcceptHeader = "application/vnd.git-lfs+json;q=0.9, */*;q=0.8"
+	// UserAgentHeader Add User-Agent for gitea's self-implemented lfs client,
+	// and the version is consistent with the latest version of git lfs can be avoided incompatibilities.
+	// Some lfs servers will check this
+	UserAgentHeader = "git-lfs/3.6.0 (Gitea)"
 )
 
 // BatchRequest contains multiple requests processed in one batch operation.
diff --git a/modules/lfs/transferadapter_test.go b/modules/lfs/transferadapter_test.go
index 7fec137efe..a430b71a5f 100644
--- a/modules/lfs/transferadapter_test.go
+++ b/modules/lfs/transferadapter_test.go
@@ -96,7 +96,7 @@ func TestBasicTransferAdapter(t *testing.T) {
 		for n, c := range cases {
 			_, err := a.Download(context.Background(), c.link)
 			if len(c.expectederror) > 0 {
-				assert.True(t, strings.Contains(err.Error(), c.expectederror), "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror)
+				assert.Contains(t, err.Error(), c.expectederror, "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror)
 			} else {
 				assert.NoError(t, err, "case %d", n)
 			}
@@ -129,7 +129,7 @@ func TestBasicTransferAdapter(t *testing.T) {
 		for n, c := range cases {
 			err := a.Upload(context.Background(), c.link, p, bytes.NewBufferString("dummy"))
 			if len(c.expectederror) > 0 {
-				assert.True(t, strings.Contains(err.Error(), c.expectederror), "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror)
+				assert.Contains(t, err.Error(), c.expectederror, "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror)
 			} else {
 				assert.NoError(t, err, "case %d", n)
 			}
@@ -162,7 +162,7 @@ func TestBasicTransferAdapter(t *testing.T) {
 		for n, c := range cases {
 			err := a.Verify(context.Background(), c.link, p)
 			if len(c.expectederror) > 0 {
-				assert.True(t, strings.Contains(err.Error(), c.expectederror), "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror)
+				assert.Contains(t, err.Error(), c.expectederror, "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror)
 			} else {
 				assert.NoError(t, err, "case %d", n)
 			}
diff --git a/modules/log/event_format.go b/modules/log/event_format.go
index d9dbebf831..0b8d1cec79 100644
--- a/modules/log/event_format.go
+++ b/modules/log/event_format.go
@@ -110,10 +110,10 @@ func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, ms
 			buf = append(buf, ' ')
 		}
 		if flags&(Ltime|Lmicroseconds) != 0 {
-			hour, min, sec := t.Clock()
+			hour, minNum, sec := t.Clock()
 			buf = itoa(buf, hour, 2)
 			buf = append(buf, ':')
-			buf = itoa(buf, min, 2)
+			buf = itoa(buf, minNum, 2)
 			buf = append(buf, ':')
 			buf = itoa(buf, sec, 2)
 			if flags&Lmicroseconds != 0 {
diff --git a/modules/log/logger_test.go b/modules/log/logger_test.go
index 70222f64f5..0de14eb411 100644
--- a/modules/log/logger_test.go
+++ b/modules/log/logger_test.go
@@ -56,7 +56,7 @@ func TestLogger(t *testing.T) {
 	logger := NewLoggerWithWriters(context.Background(), "test")
 
 	dump := logger.DumpWriters()
-	assert.EqualValues(t, 0, len(dump))
+	assert.Empty(t, dump)
 	assert.EqualValues(t, NONE, logger.GetLevel())
 	assert.False(t, logger.IsEnabled())
 
@@ -69,7 +69,7 @@ func TestLogger(t *testing.T) {
 	assert.EqualValues(t, DEBUG, logger.GetLevel())
 
 	dump = logger.DumpWriters()
-	assert.EqualValues(t, 2, len(dump))
+	assert.Len(t, dump, 2)
 
 	logger.Trace("trace-level") // this level is not logged
 	logger.Debug("debug-level")
diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go
index 9419350e61..159d712955 100644
--- a/modules/markup/html_internal_test.go
+++ b/modules/markup/html_internal_test.go
@@ -278,12 +278,12 @@ func TestRender_AutoLink(t *testing.T) {
 	test := func(input, expected string) {
 		var buffer strings.Builder
 		err := PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
-		assert.Equal(t, err, nil)
+		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
 
 		buffer.Reset()
 		err = PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
-		assert.Equal(t, err, nil)
+		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
 	}
 
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index f77db9eb38..a14c0cad59 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -78,26 +78,23 @@ func (r *GlodmarkRender) Renderer() renderer.Renderer {
 
 func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
 	if entering {
-		language, _ := c.Language()
-		if language == nil {
-			language = []byte("text")
-		}
+		languageBytes, _ := c.Language()
+		languageStr := giteautil.IfZero(string(languageBytes), "text")
 
-		languageStr := string(language)
-
-		preClasses := []string{"code-block"}
+		preClasses := "code-block"
 		if languageStr == "mermaid" || languageStr == "math" {
-			preClasses = append(preClasses, "is-loading")
+			preClasses += " is-loading"
 		}
 
-		err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<pre class="%s">`, strings.Join(preClasses, " "))
+		err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<pre class="%s">`, preClasses)
 		if err != nil {
 			return
 		}
 
-		// include language-x class as part of commonmark spec
-		// the "display" class is used by "js/markup/math.js" to render the code element as a block
-		err = r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<code class="chroma language-%s display">`, string(language))
+		// include language-x class as part of commonmark spec, "chroma" class is used to highlight the code
+		// the "display" class is used by "js/markup/math.ts" to render the code element as a block
+		// the "math.ts" strictly depends on the structure: <pre class="code-block is-loading"><code class="language-math display">...</code></pre>
+		err = r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<code class="chroma language-%s display">`, languageStr)
 		if err != nil {
 			return
 		}
@@ -128,7 +125,12 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender {
 				),
 				highlighting.WithWrapperRenderer(r.highlightingRenderer),
 			),
-			math.NewExtension(&ctx.RenderInternal, math.Enabled(setting.Markdown.EnableMath)),
+			math.NewExtension(&ctx.RenderInternal, math.Options{
+				Enabled:           setting.Markdown.EnableMath,
+				ParseDollarInline: true,
+				ParseDollarBlock:  true,
+				ParseSquareBlock:  true, // TODO: this is a bad syntax, it should be deprecated in the future (by some config options)
+			}),
 			meta.Meta,
 		),
 		goldmark.WithParserOptions(
diff --git a/modules/markup/markdown/markdown_math_test.go b/modules/markup/markdown/markdown_math_test.go
index e371b1c74a..813f050965 100644
--- a/modules/markup/markdown/markdown_math_test.go
+++ b/modules/markup/markdown/markdown_math_test.go
@@ -12,31 +12,32 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+const nl = "\n"
+
 func TestMathRender(t *testing.T) {
-	const nl = "\n"
 	testcases := []struct {
 		testcase string
 		expected string
 	}{
 		{
 			"$a$",
-			`<p><code class="language-math is-loading">a</code></p>` + nl,
+			`<p><code class="language-math">a</code></p>` + nl,
 		},
 		{
 			"$ a $",
-			`<p><code class="language-math is-loading">a</code></p>` + nl,
+			`<p><code class="language-math">a</code></p>` + nl,
 		},
 		{
 			"$a$ $b$",
-			`<p><code class="language-math is-loading">a</code> <code class="language-math is-loading">b</code></p>` + nl,
+			`<p><code class="language-math">a</code> <code class="language-math">b</code></p>` + nl,
 		},
 		{
 			`\(a\) \(b\)`,
-			`<p><code class="language-math is-loading">a</code> <code class="language-math is-loading">b</code></p>` + nl,
+			`<p><code class="language-math">a</code> <code class="language-math">b</code></p>` + nl,
 		},
 		{
 			`$a$.`,
-			`<p><code class="language-math is-loading">a</code>.</p>` + nl,
+			`<p><code class="language-math">a</code>.</p>` + nl,
 		},
 		{
 			`.$a$`,
@@ -64,27 +65,39 @@ func TestMathRender(t *testing.T) {
 		},
 		{
 			"$a$ ($b$) [$c$] {$d$}",
-			`<p><code class="language-math is-loading">a</code> (<code class="language-math is-loading">b</code>) [$c$] {$d$}</p>` + nl,
+			`<p><code class="language-math">a</code> (<code class="language-math">b</code>) [$c$] {$d$}</p>` + nl,
 		},
 		{
 			"$$a$$",
-			`<code class="chroma language-math display">a</code>` + nl,
+			`<code class="language-math display">a</code>` + nl,
 		},
 		{
 			"$$a$$ test",
-			`<p><code class="language-math display is-loading">a</code> test</p>` + nl,
+			`<p><code class="language-math">a</code> test</p>` + nl,
 		},
 		{
 			"test $$a$$",
-			`<p>test <code class="language-math display is-loading">a</code></p>` + nl,
+			`<p>test <code class="language-math">a</code></p>` + nl,
 		},
 		{
 			`foo $x=\$$ bar`,
-			`<p>foo <code class="language-math is-loading">x=\$</code> bar</p>` + nl,
+			`<p>foo <code class="language-math">x=\$</code> bar</p>` + nl,
 		},
 		{
 			`$\text{$b$}$`,
-			`<p><code class="language-math is-loading">\text{$b$}</code></p>` + nl,
+			`<p><code class="language-math">\text{$b$}</code></p>` + nl,
+		},
+		{
+			"a$`b`$c",
+			`<p>a<code class="language-math">b</code>c</p>` + nl,
+		},
+		{
+			"a $`b`$ c",
+			`<p>a <code class="language-math">b</code> c</p>` + nl,
+		},
+		{
+			"a$``b``$c x$```y```$z",
+			`<p>a<code class="language-math">b</code>c x<code class="language-math">y</code>z</p>` + nl,
 		},
 	}
 
@@ -110,7 +123,7 @@ func TestMathRenderBlockIndent(t *testing.T) {
 \alpha
 \]
 `,
-			`<pre class="code-block is-loading"><code class="chroma language-math display">
+			`<pre class="code-block is-loading"><code class="language-math display">
 \alpha
 </code></pre>
 `,
@@ -122,7 +135,7 @@ func TestMathRenderBlockIndent(t *testing.T) {
  \alpha
  \]
 `,
-			`<pre class="code-block is-loading"><code class="chroma language-math display">
+			`<pre class="code-block is-loading"><code class="language-math display">
 \alpha
 </code></pre>
 `,
@@ -137,7 +150,7 @@ a
    d
   \]
 `,
-			`<pre class="code-block is-loading"><code class="chroma language-math display">
+			`<pre class="code-block is-loading"><code class="language-math display">
 a
 b
 c
@@ -154,7 +167,7 @@ c
   c
   \]
 `,
-			`<pre class="code-block is-loading"><code class="chroma language-math display">
+			`<pre class="code-block is-loading"><code class="language-math display">
 a
  b
 c
@@ -165,7 +178,7 @@ c
 			"indent-0-oneline",
 			`$$ x $$
 foo`,
-			`<code class="chroma language-math display"> x </code>
+			`<code class="language-math display"> x </code>
 <p>foo</p>
 `,
 		},
@@ -173,7 +186,7 @@ foo`,
 			"indent-3-oneline",
 			`   $$ x $$<SPACE>
 foo`,
-			`<code class="chroma language-math display"> x </code>
+			`<code class="language-math display"> x </code>
 <p>foo</p>
 `,
 		},
@@ -188,10 +201,10 @@ foo`,
 > \]
 `,
 			`<blockquote>
-<pre class="code-block is-loading"><code class="chroma language-math display">
+<pre class="code-block is-loading"><code class="language-math display">
 a
 </code></pre>
-<pre class="code-block is-loading"><code class="chroma language-math display">
+<pre class="code-block is-loading"><code class="language-math display">
 b
 </code></pre>
 </blockquote>
@@ -207,7 +220,7 @@ b
 2. b`,
 			`<ol>
 <li>a
-<pre class="code-block is-loading"><code class="chroma language-math display">
+<pre class="code-block is-loading"><code class="language-math display">
 x
 </code></pre>
 </li>
@@ -215,6 +228,11 @@ x
 </ol>
 `,
 		},
+		{
+			"inline-non-math",
+			`\[x]`,
+			`<p>[x]</p>` + nl,
+		},
 	}
 
 	for _, test := range testcases {
diff --git a/modules/markup/markdown/math/block_parser.go b/modules/markup/markdown/math/block_parser.go
index 3f37ce8333..2c5553550a 100644
--- a/modules/markup/markdown/math/block_parser.go
+++ b/modules/markup/markdown/math/block_parser.go
@@ -16,16 +16,18 @@ import (
 
 type blockParser struct {
 	parseDollars    bool
+	parseSquare     bool
 	endBytesDollars []byte
-	endBytesBracket []byte
+	endBytesSquare  []byte
 }
 
 // NewBlockParser creates a new math BlockParser
-func NewBlockParser(parseDollarBlocks bool) parser.BlockParser {
+func NewBlockParser(parseDollars, parseSquare bool) parser.BlockParser {
 	return &blockParser{
-		parseDollars:    parseDollarBlocks,
+		parseDollars:    parseDollars,
+		parseSquare:     parseSquare,
 		endBytesDollars: []byte{'$', '$'},
-		endBytesBracket: []byte{'\\', ']'},
+		endBytesSquare:  []byte{'\\', ']'},
 	}
 }
 
@@ -40,7 +42,7 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex
 	var dollars bool
 	if b.parseDollars && line[pos] == '$' && line[pos+1] == '$' {
 		dollars = true
-	} else if line[pos] == '\\' && line[pos+1] == '[' {
+	} else if b.parseSquare && line[pos] == '\\' && line[pos+1] == '[' {
 		if len(line[pos:]) >= 3 && line[pos+2] == '!' && bytes.Contains(line[pos:], []byte(`\]`)) {
 			// do not process escaped attention block: "> \[!NOTE\]"
 			return nil, parser.NoChildren
@@ -53,10 +55,10 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex
 	node := NewBlock(dollars, pos)
 
 	// Now we need to check if the ending block is on the segment...
-	endBytes := giteaUtil.Iif(dollars, b.endBytesDollars, b.endBytesBracket)
+	endBytes := giteaUtil.Iif(dollars, b.endBytesDollars, b.endBytesSquare)
 	idx := bytes.Index(line[pos+2:], endBytes)
 	if idx >= 0 {
-		// for case $$ ... $$ any other text
+		// for case: "$$ ... $$ any other text" (this case will be handled by the inline parser)
 		for i := pos + 2 + idx + 2; i < len(line); i++ {
 			if line[i] != ' ' && line[i] != '\n' {
 				return nil, parser.NoChildren
@@ -70,6 +72,13 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex
 		return node, parser.Close | parser.NoChildren
 	}
 
+	// for case "\[ ... ]" (no close marker on the same line)
+	for i := pos + 2 + idx + 2; i < len(line); i++ {
+		if line[i] != ' ' && line[i] != '\n' {
+			return nil, parser.NoChildren
+		}
+	}
+
 	segment.Start += pos + 2
 	node.Lines().Append(segment)
 	return node, parser.NoChildren
@@ -85,7 +94,7 @@ func (b *blockParser) Continue(node ast.Node, reader text.Reader, pc parser.Cont
 	line, segment := reader.PeekLine()
 	w, pos := util.IndentWidth(line, reader.LineOffset())
 	if w < 4 {
-		endBytes := giteaUtil.Iif(block.Dollars, b.endBytesDollars, b.endBytesBracket)
+		endBytes := giteaUtil.Iif(block.Dollars, b.endBytesDollars, b.endBytesSquare)
 		if bytes.HasPrefix(line[pos:], endBytes) && util.IsBlank(line[pos+len(endBytes):]) {
 			if util.IsBlank(line[pos+len(endBytes):]) {
 				newline := giteaUtil.Iif(line[len(line)-1] != '\n', 0, 1)
diff --git a/modules/markup/markdown/math/block_renderer.go b/modules/markup/markdown/math/block_renderer.go
index a770efa01c..c29f061882 100644
--- a/modules/markup/markdown/math/block_renderer.go
+++ b/modules/markup/markdown/math/block_renderer.go
@@ -12,6 +12,17 @@ import (
 	"github.com/yuin/goldmark/util"
 )
 
+// Block render output:
+// 	<pre class="code-block is-loading"><code class="language-math display">...</code></pre>
+//
+// Keep in mind that there is another "code block" render in "func (r *GlodmarkRender) highlightingRenderer"
+// "highlightingRenderer" outputs the math block with extra "chroma" class:
+// 	<pre class="code-block is-loading"><code class="chroma language-math display">...</code></pre>
+//
+// Special classes:
+// * "is-loading": show a loading indicator
+// * "display": used by JS to decide to render as a block, otherwise render as inline
+
 // BlockRenderer represents a renderer for math Blocks
 type BlockRenderer struct {
 	renderInternal *internal.RenderInternal
@@ -38,7 +49,7 @@ func (r *BlockRenderer) writeLines(w util.BufWriter, source []byte, n gast.Node)
 func (r *BlockRenderer) renderBlock(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
 	n := node.(*Block)
 	if entering {
-		code := giteaUtil.Iif(n.Inline, "", `<pre class="code-block is-loading">`) + `<code class="chroma language-math display">`
+		code := giteaUtil.Iif(n.Inline, "", `<pre class="code-block is-loading">`) + `<code class="language-math display">`
 		_ = r.renderInternal.FormatWithSafeAttrs(w, code)
 		r.writeLines(w, source, n)
 	} else {
diff --git a/modules/markup/markdown/math/inline_block_node.go b/modules/markup/markdown/math/inline_block_node.go
deleted file mode 100644
index c92d0c8d84..0000000000
--- a/modules/markup/markdown/math/inline_block_node.go
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2024 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package math
-
-import (
-	"github.com/yuin/goldmark/ast"
-)
-
-// InlineBlock represents inline math e.g. $$...$$
-type InlineBlock struct {
-	Inline
-}
-
-// InlineBlock implements InlineBlock.
-func (n *InlineBlock) InlineBlock() {}
-
-// KindInlineBlock is the kind for math inline block
-var KindInlineBlock = ast.NewNodeKind("MathInlineBlock")
-
-// Kind returns KindInlineBlock
-func (n *InlineBlock) Kind() ast.NodeKind {
-	return KindInlineBlock
-}
-
-// NewInlineBlock creates a new ast math inline block node
-func NewInlineBlock() *InlineBlock {
-	return &InlineBlock{
-		Inline{},
-	}
-}
diff --git a/modules/markup/markdown/math/inline_node.go b/modules/markup/markdown/math/inline_node.go
index 2221a251bf..1e4034d54b 100644
--- a/modules/markup/markdown/math/inline_node.go
+++ b/modules/markup/markdown/math/inline_node.go
@@ -8,7 +8,7 @@ import (
 	"github.com/yuin/goldmark/util"
 )
 
-// Inline represents inline math e.g. $...$ or \(...\)
+// Inline struct represents inline math e.g. $...$ or \(...\)
 type Inline struct {
 	ast.BaseInline
 }
diff --git a/modules/markup/markdown/math/inline_parser.go b/modules/markup/markdown/math/inline_parser.go
index 191d1e5a31..a57abe9f9b 100644
--- a/modules/markup/markdown/math/inline_parser.go
+++ b/modules/markup/markdown/math/inline_parser.go
@@ -12,31 +12,25 @@ import (
 )
 
 type inlineParser struct {
-	start []byte
-	end   []byte
+	trigger              []byte
+	endBytesSingleDollar []byte
+	endBytesDoubleDollar []byte
+	endBytesBracket      []byte
 }
 
 var defaultInlineDollarParser = &inlineParser{
-	start: []byte{'$'},
-	end:   []byte{'$'},
-}
-
-var defaultDualDollarParser = &inlineParser{
-	start: []byte{'$', '$'},
-	end:   []byte{'$', '$'},
+	trigger:              []byte{'$'},
+	endBytesSingleDollar: []byte{'$'},
+	endBytesDoubleDollar: []byte{'$', '$'},
 }
 
 func NewInlineDollarParser() parser.InlineParser {
 	return defaultInlineDollarParser
 }
 
-func NewInlineDualDollarParser() parser.InlineParser {
-	return defaultDualDollarParser
-}
-
 var defaultInlineBracketParser = &inlineParser{
-	start: []byte{'\\', '('},
-	end:   []byte{'\\', ')'},
+	trigger:         []byte{'\\', '('},
+	endBytesBracket: []byte{'\\', ')'},
 }
 
 func NewInlineBracketParser() parser.InlineParser {
@@ -45,7 +39,7 @@ func NewInlineBracketParser() parser.InlineParser {
 
 // Trigger triggers this parser on $ or \
 func (parser *inlineParser) Trigger() []byte {
-	return parser.start
+	return parser.trigger
 }
 
 func isPunctuation(b byte) bool {
@@ -64,33 +58,60 @@ func isAlphanumeric(b byte) bool {
 func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
 	line, _ := block.PeekLine()
 
-	if !bytes.HasPrefix(line, parser.start) {
+	if !bytes.HasPrefix(line, parser.trigger) {
 		// We'll catch this one on the next time round
 		return nil
 	}
 
-	precedingCharacter := block.PrecendingCharacter()
-	if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
-		// need to exclude things like `a$` from being considered a start
-		return nil
+	var startMarkLen int
+	var stopMark []byte
+	checkSurrounding := true
+	if line[0] == '$' {
+		startMarkLen = 1
+		stopMark = parser.endBytesSingleDollar
+		if len(line) > 1 {
+			if line[1] == '$' {
+				startMarkLen = 2
+				stopMark = parser.endBytesDoubleDollar
+			} else if line[1] == '`' {
+				pos := 1
+				for ; pos < len(line) && line[pos] == '`'; pos++ {
+				}
+				startMarkLen = pos
+				stopMark = bytes.Repeat([]byte{'`'}, pos)
+				stopMark[len(stopMark)-1] = '$'
+				checkSurrounding = false
+			}
+		}
+	} else {
+		startMarkLen = 2
+		stopMark = parser.endBytesBracket
+	}
+
+	if checkSurrounding {
+		precedingCharacter := block.PrecendingCharacter()
+		if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
+			// need to exclude things like `a$` from being considered a start
+			return nil
+		}
 	}
 
 	// move the opener marker point at the start of the text
-	opener := len(parser.start)
+	opener := startMarkLen
 
 	// Now look for an ending line
 	depth := 0
 	ender := -1
 	for i := opener; i < len(line); i++ {
-		if depth == 0 && bytes.HasPrefix(line[i:], parser.end) {
+		if depth == 0 && bytes.HasPrefix(line[i:], stopMark) {
 			succeedingCharacter := byte(0)
-			if i+len(parser.end) < len(line) {
-				succeedingCharacter = line[i+len(parser.end)]
+			if i+len(stopMark) < len(line) {
+				succeedingCharacter = line[i+len(stopMark)]
 			}
 			// check valid ending character
 			isValidEndingChar := isPunctuation(succeedingCharacter) || isBracket(succeedingCharacter) ||
 				succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0
-			if !isValidEndingChar {
+			if checkSurrounding && !isValidEndingChar {
 				break
 			}
 			ender = i
@@ -112,21 +133,12 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
 
 	block.Advance(opener)
 	_, pos := block.Position()
-	var node ast.Node
-	if parser == defaultDualDollarParser {
-		node = NewInlineBlock()
-	} else {
-		node = NewInline()
-	}
+	node := NewInline()
+
 	segment := pos.WithStop(pos.Start + ender - opener)
 	node.AppendChild(node, ast.NewRawTextSegment(segment))
-	block.Advance(ender - opener + len(parser.end))
-
-	if parser == defaultDualDollarParser {
-		trimBlock(&(node.(*InlineBlock)).Inline, block)
-	} else {
-		trimBlock(node.(*Inline), block)
-	}
+	block.Advance(ender - opener + len(stopMark))
+	trimBlock(node, block)
 	return node
 }
 
diff --git a/modules/markup/markdown/math/inline_renderer.go b/modules/markup/markdown/math/inline_renderer.go
index 0cff4f1e74..d000a7b317 100644
--- a/modules/markup/markdown/math/inline_renderer.go
+++ b/modules/markup/markdown/math/inline_renderer.go
@@ -13,6 +13,9 @@ import (
 	"github.com/yuin/goldmark/util"
 )
 
+// Inline render output:
+// <code class="language-math">...</code>
+
 // InlineRenderer is an inline renderer
 type InlineRenderer struct {
 	renderInternal *internal.RenderInternal
@@ -25,11 +28,7 @@ func NewInlineRenderer(renderInternal *internal.RenderInternal) renderer.NodeRen
 
 func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
 	if entering {
-		extraClass := ""
-		if _, ok := n.(*InlineBlock); ok {
-			extraClass = "display "
-		}
-		_ = r.renderInternal.FormatWithSafeAttrs(w, `<code class="language-math %sis-loading">`, extraClass)
+		_ = r.renderInternal.FormatWithSafeAttrs(w, `<code class="language-math">`)
 		for c := n.FirstChild(); c != nil; c = c.NextSibling() {
 			segment := c.(*ast.Text).Segment
 			value := util.EscapeHTML(segment.Value(source))
@@ -51,5 +50,4 @@ func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Nod
 // RegisterFuncs registers the renderer for inline math nodes
 func (r *InlineRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
 	reg.Register(KindInline, r.renderInline)
-	reg.Register(KindInlineBlock, r.renderInline)
 }
diff --git a/modules/markup/markdown/math/math.go b/modules/markup/markdown/math/math.go
index 7e8defcd4a..a6ff593d62 100644
--- a/modules/markup/markdown/math/math.go
+++ b/modules/markup/markdown/math/math.go
@@ -5,6 +5,7 @@ package math
 
 import (
 	"code.gitea.io/gitea/modules/markup/internal"
+	giteaUtil "code.gitea.io/gitea/modules/util"
 
 	"github.com/yuin/goldmark"
 	"github.com/yuin/goldmark/parser"
@@ -12,70 +13,45 @@ import (
 	"github.com/yuin/goldmark/util"
 )
 
+type Options struct {
+	Enabled           bool
+	ParseDollarInline bool
+	ParseDollarBlock  bool
+	ParseSquareBlock  bool
+}
+
 // Extension is a math extension
 type Extension struct {
-	renderInternal    *internal.RenderInternal
-	enabled           bool
-	parseDollarInline bool
-	parseDollarBlock  bool
-}
-
-// Option is the interface Options should implement
-type Option interface {
-	SetOption(e *Extension)
-}
-
-type extensionFunc func(e *Extension)
-
-func (fn extensionFunc) SetOption(e *Extension) {
-	fn(e)
-}
-
-// Enabled enables or disables this extension
-func Enabled(enable ...bool) Option {
-	value := true
-	if len(enable) > 0 {
-		value = enable[0]
-	}
-	return extensionFunc(func(e *Extension) {
-		e.enabled = value
-	})
+	renderInternal *internal.RenderInternal
+	options        Options
 }
 
 // NewExtension creates a new math extension with the provided options
-func NewExtension(renderInternal *internal.RenderInternal, opts ...Option) *Extension {
+func NewExtension(renderInternal *internal.RenderInternal, opts ...Options) *Extension {
+	opt := giteaUtil.OptionalArg(opts)
 	r := &Extension{
-		renderInternal:    renderInternal,
-		enabled:           true,
-		parseDollarBlock:  true,
-		parseDollarInline: true,
-	}
-
-	for _, o := range opts {
-		o.SetOption(r)
+		renderInternal: renderInternal,
+		options:        opt,
 	}
 	return r
 }
 
 // Extend extends goldmark with our parsers and renderers
 func (e *Extension) Extend(m goldmark.Markdown) {
-	if !e.enabled {
+	if !e.options.Enabled {
 		return
 	}
 
-	m.Parser().AddOptions(parser.WithBlockParsers(
-		util.Prioritized(NewBlockParser(e.parseDollarBlock), 701),
-	))
-
-	inlines := []util.PrioritizedValue{
-		util.Prioritized(NewInlineBracketParser(), 501),
-	}
-	if e.parseDollarInline {
-		inlines = append(inlines, util.Prioritized(NewInlineDollarParser(), 503),
-			util.Prioritized(NewInlineDualDollarParser(), 502))
+	inlines := []util.PrioritizedValue{util.Prioritized(NewInlineBracketParser(), 501)}
+	if e.options.ParseDollarInline {
+		inlines = append(inlines, util.Prioritized(NewInlineDollarParser(), 502))
 	}
 	m.Parser().AddOptions(parser.WithInlineParsers(inlines...))
 
+	m.Parser().AddOptions(parser.WithBlockParsers(
+		util.Prioritized(NewBlockParser(e.options.ParseDollarBlock, e.options.ParseSquareBlock), 701),
+	))
+
 	m.Renderer().AddOptions(renderer.WithNodeRenderers(
 		util.Prioritized(NewBlockRenderer(e.renderInternal), 501),
 		util.Prioritized(NewInlineRenderer(e.renderInternal), 502),
diff --git a/modules/packages/conan/conanfile_parser_test.go b/modules/packages/conan/conanfile_parser_test.go
index 5801570184..aabafd5f64 100644
--- a/modules/packages/conan/conanfile_parser_test.go
+++ b/modules/packages/conan/conanfile_parser_test.go
@@ -40,7 +40,7 @@ class ConanPackageConan(ConanFile):
 
 func TestParseConanfile(t *testing.T) {
 	metadata, err := ParseConanfile(strings.NewReader(contentConanfile))
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	assert.Equal(t, license, metadata.License)
 	assert.Equal(t, author, metadata.Author)
 	assert.Equal(t, homepage, metadata.ProjectURL)
diff --git a/modules/packages/conan/conaninfo_parser_test.go b/modules/packages/conan/conaninfo_parser_test.go
index 556a4b939e..f6510ca667 100644
--- a/modules/packages/conan/conaninfo_parser_test.go
+++ b/modules/packages/conan/conaninfo_parser_test.go
@@ -50,7 +50,7 @@ const (
 func TestParseConaninfo(t *testing.T) {
 	info, err := ParseConaninfo(strings.NewReader(contentConaninfo))
 	assert.NotNil(t, info)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	assert.Equal(
 		t,
 		map[string]string{
diff --git a/modules/queue/base_test.go b/modules/queue/base_test.go
index c5bf526ae6..01b52b3c16 100644
--- a/modules/queue/base_test.go
+++ b/modules/queue/base_test.go
@@ -46,10 +46,10 @@ func testQueueBasic(t *testing.T, newFn func(cfg *BaseConfig) (baseQueue, error)
 		assert.NoError(t, err)
 		if !isUnique {
 			assert.EqualValues(t, 2, cnt)
-			assert.EqualValues(t, false, has) // non-unique queues don't check for duplicates
+			assert.False(t, has) // non-unique queues don't check for duplicates
 		} else {
 			assert.EqualValues(t, 1, cnt)
-			assert.EqualValues(t, true, has)
+			assert.True(t, has)
 		}
 
 		// push another item
@@ -101,7 +101,7 @@ func testQueueBasic(t *testing.T, newFn func(cfg *BaseConfig) (baseQueue, error)
 		pushBlockTime = 30 * time.Millisecond
 		err = q.PushItem(ctx, []byte("item-full"))
 		assert.ErrorIs(t, err, context.DeadlineExceeded)
-		assert.True(t, time.Since(timeStart) >= pushBlockTime*2/3)
+		assert.GreaterOrEqual(t, time.Since(timeStart), pushBlockTime*2/3)
 		pushBlockTime = oldPushBlockTime
 
 		// remove all
diff --git a/modules/queue/workerqueue_test.go b/modules/queue/workerqueue_test.go
index d66253ff66..c0841a1752 100644
--- a/modules/queue/workerqueue_test.go
+++ b/modules/queue/workerqueue_test.go
@@ -172,8 +172,8 @@ func testWorkerPoolQueuePersistence(t *testing.T, queueSetting setting.QueueSett
 
 	q2() // restart the queue to continue to execute the tasks in it
 
-	assert.NotZero(t, len(tasksQ1))
-	assert.NotZero(t, len(tasksQ2))
+	assert.NotEmpty(t, tasksQ1)
+	assert.NotEmpty(t, tasksQ2)
 	assert.EqualValues(t, testCount, len(tasksQ1)+len(tasksQ2))
 }
 
diff --git a/modules/references/references.go b/modules/references/references.go
index 2889430bcf..6e549cb875 100644
--- a/modules/references/references.go
+++ b/modules/references/references.go
@@ -164,9 +164,9 @@ func newKeywords() {
 	})
 }
 
-func doNewKeywords(close, reopen []string) {
-	issueCloseKeywordsPat = makeKeywordsPat(close)
-	issueReopenKeywordsPat = makeKeywordsPat(reopen)
+func doNewKeywords(closeKeywords, reopenKeywords []string) {
+	issueCloseKeywordsPat = makeKeywordsPat(closeKeywords)
+	issueReopenKeywordsPat = makeKeywordsPat(reopenKeywords)
 }
 
 // getGiteaHostName returns a normalized string with the local host name, with no scheme or port information
diff --git a/modules/references/references_test.go b/modules/references/references_test.go
index e5a0d60fe3..e224c919e9 100644
--- a/modules/references/references_test.go
+++ b/modules/references/references_test.go
@@ -526,7 +526,7 @@ func TestCustomizeCloseKeywords(t *testing.T) {
 
 func TestParseCloseKeywords(t *testing.T) {
 	// Test parsing of CloseKeywords and ReopenKeywords
-	assert.Len(t, parseKeywords([]string{""}), 0)
+	assert.Empty(t, parseKeywords([]string{""}))
 	assert.Len(t, parseKeywords([]string{"  aa  ", " bb  ", "99", "#", "", "this is", "cc"}), 3)
 
 	for _, test := range []struct {
diff --git a/modules/repository/fork.go b/modules/repository/fork.go
index fbf0008716..d530634071 100644
--- a/modules/repository/fork.go
+++ b/modules/repository/fork.go
@@ -9,14 +9,22 @@ import (
 	"code.gitea.io/gitea/models/organization"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/setting"
 )
 
+func CanUserForkBetweenOwners(id1, id2 int64) bool {
+	if id1 != id2 {
+		return true
+	}
+	return setting.Repository.AllowForkIntoSameOwner
+}
+
 // CanUserForkRepo returns true if specified user can fork repository.
 func CanUserForkRepo(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (bool, error) {
 	if user == nil {
 		return false, nil
 	}
-	if repo.OwnerID != user.ID && !repo_model.HasForkedRepo(ctx, user.ID, repo.ID) {
+	if CanUserForkBetweenOwners(repo.OwnerID, user.ID) && !repo_model.HasForkedRepo(ctx, user.ID, repo.ID) {
 		return true, nil
 	}
 	ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, user.ID)
diff --git a/modules/repository/fork_test.go b/modules/repository/fork_test.go
new file mode 100644
index 0000000000..f8c76d942d
--- /dev/null
+++ b/modules/repository/fork_test.go
@@ -0,0 +1,25 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCanUserForkBetweenOwners(t *testing.T) {
+	defer test.MockVariableValue(&setting.Repository.AllowForkIntoSameOwner)
+
+	setting.Repository.AllowForkIntoSameOwner = true
+	assert.True(t, CanUserForkBetweenOwners(1, 1))
+	assert.True(t, CanUserForkBetweenOwners(1, 2))
+
+	setting.Repository.AllowForkIntoSameOwner = false
+	assert.False(t, CanUserForkBetweenOwners(1, 1))
+	assert.True(t, CanUserForkBetweenOwners(1, 2))
+}
diff --git a/modules/repository/repo_test.go b/modules/repository/repo_test.go
index 68980f92f9..f3e7be6d7d 100644
--- a/modules/repository/repo_test.go
+++ b/modules/repository/repo_test.go
@@ -62,15 +62,15 @@ func Test_calcSync(t *testing.T) {
 	}
 
 	inserts, deletes, updates := calcSync(gitTags, dbReleases)
-	if assert.EqualValues(t, 1, len(inserts), "inserts") {
+	if assert.Len(t, inserts, 1, "inserts") {
 		assert.EqualValues(t, *gitTags[2], *inserts[0], "inserts equal")
 	}
 
-	if assert.EqualValues(t, 1, len(deletes), "deletes") {
+	if assert.Len(t, deletes, 1, "deletes") {
 		assert.EqualValues(t, 1, deletes[0], "deletes equal")
 	}
 
-	if assert.EqualValues(t, 1, len(updates), "updates") {
+	if assert.Len(t, updates, 1, "updates") {
 		assert.EqualValues(t, *gitTags[1], *updates[0], "updates equal")
 	}
 }
diff --git a/modules/setting/cron_test.go b/modules/setting/cron_test.go
index 3187ab18a2..55244d7075 100644
--- a/modules/setting/cron_test.go
+++ b/modules/setting/cron_test.go
@@ -38,6 +38,6 @@ EXTEND = true
 	_, err = getCronSettings(cfg, "test", extended)
 	assert.NoError(t, err)
 	assert.True(t, extended.Base)
-	assert.EqualValues(t, extended.Second, "white rabbit")
+	assert.EqualValues(t, "white rabbit", extended.Second)
 	assert.True(t, extended.Extend)
 }
diff --git a/modules/setting/oauth2_test.go b/modules/setting/oauth2_test.go
index 38ee4d248d..d0e5ccf13d 100644
--- a/modules/setting/oauth2_test.go
+++ b/modules/setting/oauth2_test.go
@@ -74,5 +74,5 @@ DEFAULT_APPLICATIONS = tea
 DEFAULT_APPLICATIONS =
 `)
 	loadOAuth2From(cfg)
-	assert.Nil(t, nil, OAuth2.DefaultApplications)
+	assert.Nil(t, OAuth2.DefaultApplications)
 }
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index 14cf5805c0..c5619d0f04 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -53,6 +53,7 @@ var (
 		AllowDeleteOfUnadoptedRepositories      bool
 		DisableDownloadSourceArchives           bool
 		AllowForkWithoutMaximumLimit            bool
+		AllowForkIntoSameOwner                  bool
 
 		// Repository editor settings
 		Editor struct {
diff --git a/modules/setting/storage_test.go b/modules/setting/storage_test.go
index 8ee37fd2b6..afff85537e 100644
--- a/modules/setting/storage_test.go
+++ b/modules/setting/storage_test.go
@@ -447,7 +447,7 @@ MINIO_USE_SSL = true
 	assert.NoError(t, loadRepoArchiveFrom(cfg))
 	assert.EqualValues(t, "my_access_key", RepoArchive.Storage.MinioConfig.AccessKeyID)
 	assert.EqualValues(t, "my_secret_key", RepoArchive.Storage.MinioConfig.SecretAccessKey)
-	assert.EqualValues(t, true, RepoArchive.Storage.MinioConfig.UseSSL)
+	assert.True(t, RepoArchive.Storage.MinioConfig.UseSSL)
 	assert.EqualValues(t, "repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
 }
 
@@ -464,7 +464,7 @@ MINIO_BASE_PATH = /prefix
 	assert.NoError(t, loadRepoArchiveFrom(cfg))
 	assert.EqualValues(t, "my_access_key", RepoArchive.Storage.MinioConfig.AccessKeyID)
 	assert.EqualValues(t, "my_secret_key", RepoArchive.Storage.MinioConfig.SecretAccessKey)
-	assert.EqualValues(t, true, RepoArchive.Storage.MinioConfig.UseSSL)
+	assert.True(t, RepoArchive.Storage.MinioConfig.UseSSL)
 	assert.EqualValues(t, "/prefix/repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
 
 	cfg, err = NewConfigProviderFromData(`
@@ -477,7 +477,7 @@ MINIO_BASE_PATH = /prefix
 	assert.NoError(t, err)
 	assert.NoError(t, loadRepoArchiveFrom(cfg))
 	assert.EqualValues(t, "127.0.0.1", RepoArchive.Storage.MinioConfig.IamEndpoint)
-	assert.EqualValues(t, true, RepoArchive.Storage.MinioConfig.UseSSL)
+	assert.True(t, RepoArchive.Storage.MinioConfig.UseSSL)
 	assert.EqualValues(t, "/prefix/repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
 
 	cfg, err = NewConfigProviderFromData(`
@@ -495,7 +495,7 @@ MINIO_BASE_PATH = /lfs
 	assert.NoError(t, loadLFSFrom(cfg))
 	assert.EqualValues(t, "my_access_key", LFS.Storage.MinioConfig.AccessKeyID)
 	assert.EqualValues(t, "my_secret_key", LFS.Storage.MinioConfig.SecretAccessKey)
-	assert.EqualValues(t, true, LFS.Storage.MinioConfig.UseSSL)
+	assert.True(t, LFS.Storage.MinioConfig.UseSSL)
 	assert.EqualValues(t, "/lfs", LFS.Storage.MinioConfig.BasePath)
 
 	cfg, err = NewConfigProviderFromData(`
@@ -513,7 +513,7 @@ MINIO_BASE_PATH = /lfs
 	assert.NoError(t, loadLFSFrom(cfg))
 	assert.EqualValues(t, "my_access_key", LFS.Storage.MinioConfig.AccessKeyID)
 	assert.EqualValues(t, "my_secret_key", LFS.Storage.MinioConfig.SecretAccessKey)
-	assert.EqualValues(t, true, LFS.Storage.MinioConfig.UseSSL)
+	assert.True(t, LFS.Storage.MinioConfig.UseSSL)
 	assert.EqualValues(t, "/lfs", LFS.Storage.MinioConfig.BasePath)
 }
 
diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go
index f8e4f569b8..7479cfbd95 100644
--- a/modules/ssh/ssh.go
+++ b/modules/ssh/ssh.go
@@ -17,6 +17,7 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"reflect"
 	"strconv"
 	"strings"
 	"sync"
@@ -33,9 +34,26 @@ import (
 	gossh "golang.org/x/crypto/ssh"
 )
 
-type contextKey string
+// The ssh auth overall works like this:
+// NewServerConn:
+//	serverHandshake+serverAuthenticate:
+//		PublicKeyCallback:
+//			PublicKeyHandler (our code):
+//				reset(ctx.Permissions) and set ctx.Permissions.giteaKeyID = keyID
+//		pubKey.Verify
+//		return ctx.Permissions // only reaches here, the pub key is really authenticated
+//	set conn.Permissions from serverAuthenticate
+//  sessionHandler(conn)
+//
+// Then sessionHandler should only use the "verified keyID" from the original ssh conn, but not the ctx one.
+// Otherwise, if a user provides 2 keys A (a correct one) and B (public key matches but no private key),
+// then only A succeeds to authenticate, sessionHandler will see B's keyID
+//
+// After x/crypto >= 0.31.0 (fix CVE-2024-45337), the PublicKeyCallback will be called again for the verified key,
+// it mitigates the misuse for most cases, it's still good for us to make sure we don't rely on that mitigation
+// and do not misuse the PublicKeyCallback: we should only use the verified keyID from the verified ssh conn.
 
-const giteaKeyID = contextKey("gitea-key-id")
+const giteaPermissionExtensionKeyID = "gitea-perm-ext-key-id"
 
 func getExitStatusFromError(err error) int {
 	if err == nil {
@@ -61,8 +79,32 @@ func getExitStatusFromError(err error) int {
 	return waitStatus.ExitStatus()
 }
 
+// sessionPartial is the private struct from "gliderlabs/ssh/session.go"
+// We need to read the original "conn" field from "ssh.Session interface" which contains the "*session pointer"
+// https://github.com/gliderlabs/ssh/blob/d137aad99cd6f2d9495bfd98c755bec4e5dffb8c/session.go#L109-L113
+// If upstream fixes the problem and/or changes the struct, we need to follow.
+// If the struct mismatches, the builtin ssh server will fail during integration tests.
+type sessionPartial struct {
+	sync.Mutex
+	gossh.Channel
+	conn *gossh.ServerConn
+}
+
+func ptr[T any](intf any) *T {
+	// https://pkg.go.dev/unsafe#Pointer
+	// (1) Conversion of a *T1 to Pointer to *T2.
+	// Provided that T2 is no larger than T1 and that the two share an equivalent memory layout,
+	// this conversion allows reinterpreting data of one type as data of another type.
+	v := reflect.ValueOf(intf)
+	p := v.UnsafePointer()
+	return (*T)(p)
+}
+
 func sessionHandler(session ssh.Session) {
-	keyID := fmt.Sprintf("%d", session.Context().Value(giteaKeyID).(int64))
+	// here can't use session.Permissions() because it only uses the value from ctx, which might not be the authenticated one.
+	// so we must use the original ssh conn, which always contains the correct (verified) keyID.
+	sshSession := ptr[sessionPartial](session)
+	keyID := sshSession.conn.Permissions.Extensions[giteaPermissionExtensionKeyID]
 
 	command := session.RawCommand()
 
@@ -164,6 +206,20 @@ func sessionHandler(session ssh.Session) {
 }
 
 func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
+	// The publicKeyHandler (PublicKeyCallback) only helps to provide the candidate keys to authenticate,
+	// It does NOT really verify here, so we could only record the related information here.
+	// After authentication (Verify), the "Permissions" will be assigned to the ssh conn,
+	// then we can use it in the "session handler"
+
+	// first, reset the ctx permissions (just like https://github.com/gliderlabs/ssh/pull/243 does)
+	// it shouldn't be reused across different ssh conn (sessions), each pub key should have its own "Permissions"
+	ctx.Permissions().Permissions = &gossh.Permissions{}
+	setPermExt := func(keyID int64) {
+		ctx.Permissions().Permissions.Extensions = map[string]string{
+			giteaPermissionExtensionKeyID: fmt.Sprint(keyID),
+		}
+	}
+
 	if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
 		log.Debug("Handle Public Key: Fingerprint: %s from %s", gossh.FingerprintSHA256(key), ctx.RemoteAddr())
 	}
@@ -238,8 +294,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
 			if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
 				log.Debug("Successfully authenticated: %s Certificate Fingerprint: %s Principal: %s", ctx.RemoteAddr(), gossh.FingerprintSHA256(key), principal)
 			}
-			ctx.SetValue(giteaKeyID, pkey.ID)
-
+			setPermExt(pkey.ID)
 			return true
 		}
 
@@ -266,8 +321,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
 	if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary
 		log.Debug("Successfully authenticated: %s Public Key Fingerprint: %s", ctx.RemoteAddr(), gossh.FingerprintSHA256(key))
 	}
-	ctx.SetValue(giteaKeyID, pkey.ID)
-
+	setPermExt(pkey.ID)
 	return true
 }
 
diff --git a/modules/templates/util_date.go b/modules/templates/util_date.go
index 66f83d23fe..658691ee40 100644
--- a/modules/templates/util_date.go
+++ b/modules/templates/util_date.go
@@ -53,8 +53,8 @@ func parseLegacy(datetime string) time.Time {
 	return t
 }
 
-func anyToTime(any any) (t time.Time, isZero bool) {
-	switch v := any.(type) {
+func anyToTime(value any) (t time.Time, isZero bool) {
+	switch v := value.(type) {
 	case nil:
 		// it is zero
 	case *time.Time:
@@ -72,7 +72,7 @@ func anyToTime(any any) (t time.Time, isZero bool) {
 	case int64:
 		t = timeutil.TimeStamp(v).AsTime()
 	default:
-		panic(fmt.Sprintf("Unsupported time type %T", any))
+		panic(fmt.Sprintf("Unsupported time type %T", value))
 	}
 	return t, t.IsZero() || t.Unix() == 0
 }
diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go
index 479b755da1..382e2de13f 100644
--- a/modules/templates/util_string.go
+++ b/modules/templates/util_string.go
@@ -53,10 +53,14 @@ func (su *StringUtils) Cut(s, sep string) []any {
 	return []any{before, after, found}
 }
 
-func (su *StringUtils) EllipsisString(s string, max int) string {
-	return base.EllipsisString(s, max)
+func (su *StringUtils) EllipsisString(s string, maxLength int) string {
+	return base.EllipsisString(s, maxLength)
 }
 
 func (su *StringUtils) ToUpper(s string) string {
 	return strings.ToUpper(s)
 }
+
+func (su *StringUtils) TrimPrefix(s, prefix string) string {
+	return strings.TrimPrefix(s, prefix)
+}
diff --git a/modules/user/user_test.go b/modules/user/user_test.go
index 9129ae79a1..372a675d34 100644
--- a/modules/user/user_test.go
+++ b/modules/user/user_test.go
@@ -4,7 +4,6 @@
 package user
 
 import (
-	"os"
 	"os/exec"
 	"runtime"
 	"strings"
@@ -36,7 +35,7 @@ func TestCurrentUsername(t *testing.T) {
 	if user != whoami {
 		t.Errorf("expected %s as user, got: %s", whoami, user)
 	}
-	os.Setenv("USER", "spoofed")
+	t.Setenv("USER", "spoofed")
 	user = CurrentUsername()
 	if user != whoami {
 		t.Errorf("expected %s as user, got: %s", whoami, user)
diff --git a/modules/util/color_test.go b/modules/util/color_test.go
index be6e6b122a..abd5551218 100644
--- a/modules/util/color_test.go
+++ b/modules/util/color_test.go
@@ -27,9 +27,9 @@ func Test_HexToRBGColor(t *testing.T) {
 	}
 	for n, c := range cases {
 		r, g, b := HexToRBGColor(c.colorString)
-		assert.Equal(t, c.expectedR, r, "case %d: error R should match: expected %f, but get %f", n, c.expectedR, r)
-		assert.Equal(t, c.expectedG, g, "case %d: error G should match: expected %f, but get %f", n, c.expectedG, g)
-		assert.Equal(t, c.expectedB, b, "case %d: error B should match: expected %f, but get %f", n, c.expectedB, b)
+		assert.InDelta(t, c.expectedR, r, 0, "case %d: error R should match: expected %f, but get %f", n, c.expectedR, r)
+		assert.InDelta(t, c.expectedG, g, 0, "case %d: error G should match: expected %f, but get %f", n, c.expectedG, g)
+		assert.InDelta(t, c.expectedB, b, 0, "case %d: error B should match: expected %f, but get %f", n, c.expectedB, b)
 	}
 }
 
diff --git a/modules/util/keypair_test.go b/modules/util/keypair_test.go
index c6f68c845a..2bade3bb28 100644
--- a/modules/util/keypair_test.go
+++ b/modules/util/keypair_test.go
@@ -10,7 +10,6 @@ import (
 	"crypto/sha256"
 	"crypto/x509"
 	"encoding/pem"
-	"regexp"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -23,8 +22,8 @@ func TestKeygen(t *testing.T) {
 	assert.NotEmpty(t, priv)
 	assert.NotEmpty(t, pub)
 
-	assert.Regexp(t, regexp.MustCompile("^-----BEGIN RSA PRIVATE KEY-----.*"), priv)
-	assert.Regexp(t, regexp.MustCompile("^-----BEGIN PUBLIC KEY-----.*"), pub)
+	assert.Regexp(t, "^-----BEGIN RSA PRIVATE KEY-----.*", priv)
+	assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----.*", pub)
 }
 
 func TestSignUsingKeys(t *testing.T) {
diff --git a/modules/util/time_str_test.go b/modules/util/time_str_test.go
index 67b7978d0b..8d1de51c8e 100644
--- a/modules/util/time_str_test.go
+++ b/modules/util/time_str_test.go
@@ -27,9 +27,9 @@ func TestTimeStr(t *testing.T) {
 			t.Run(test.input, func(t *testing.T) {
 				output, err := TimeEstimateParse(test.input)
 				if test.err {
-					assert.NotNil(t, err)
+					assert.Error(t, err)
 				} else {
-					assert.Nil(t, err)
+					assert.NoError(t, err)
 				}
 				assert.Equal(t, test.output, output)
 			})
diff --git a/modules/util/util_test.go b/modules/util/util_test.go
index 9ce72fb866..5abce08b41 100644
--- a/modules/util/util_test.go
+++ b/modules/util/util_test.go
@@ -122,8 +122,8 @@ func Test_NormalizeEOL(t *testing.T) {
 
 func Test_RandomInt(t *testing.T) {
 	randInt, err := CryptoRandomInt(255)
-	assert.True(t, randInt >= 0)
-	assert.True(t, randInt <= 255)
+	assert.GreaterOrEqual(t, randInt, int64(0))
+	assert.LessOrEqual(t, randInt, int64(255))
 	assert.NoError(t, err)
 }
 
@@ -223,22 +223,22 @@ func BenchmarkToUpper(b *testing.B) {
 }
 
 func TestToTitleCase(t *testing.T) {
-	assert.Equal(t, ToTitleCase(`foo bar baz`), `Foo Bar Baz`)
-	assert.Equal(t, ToTitleCase(`FOO BAR BAZ`), `Foo Bar Baz`)
+	assert.Equal(t, `Foo Bar Baz`, ToTitleCase(`foo bar baz`))
+	assert.Equal(t, `Foo Bar Baz`, ToTitleCase(`FOO BAR BAZ`))
 }
 
 func TestToPointer(t *testing.T) {
 	assert.Equal(t, "abc", *ToPointer("abc"))
 	assert.Equal(t, 123, *ToPointer(123))
 	abc := "abc"
-	assert.False(t, &abc == ToPointer(abc))
+	assert.NotSame(t, &abc, ToPointer(abc))
 	val123 := 123
-	assert.False(t, &val123 == ToPointer(val123))
+	assert.NotSame(t, &val123, ToPointer(val123))
 }
 
 func TestReserveLineBreakForTextarea(t *testing.T) {
-	assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata"), "test\ndata")
-	assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata\r\n"), "test\ndata\n")
+	assert.Equal(t, "test\ndata", ReserveLineBreakForTextarea("test\r\ndata"))
+	assert.Equal(t, "test\ndata\n", ReserveLineBreakForTextarea("test\r\ndata\r\n"))
 }
 
 func TestOptionalArg(t *testing.T) {
diff --git a/options/license/MIPS b/options/license/MIPS
new file mode 100644
index 0000000000..cf57a05639
--- /dev/null
+++ b/options/license/MIPS
@@ -0,0 +1,4 @@
+Copyright (c) 1992, 1991, 1990 MIPS Computer Systems, Inc.
+MIPS Computer Systems, Inc. grants reproduction and use
+rights to all parties, PROVIDED that this comment is
+maintained in the copy.
diff --git a/options/license/ThirdEye b/options/license/ThirdEye
new file mode 100644
index 0000000000..ce75b566e3
--- /dev/null
+++ b/options/license/ThirdEye
@@ -0,0 +1,7 @@
+(C) Copyright 1984 by Third Eye Software, Inc.
+
+Third Eye Software, Inc. grants reproduction and use rights to
+all parties, PROVIDED that this comment is maintained in the copy.
+
+Third Eye makes no claims about the applicability of this
+symbol table to a particular use.
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index f50ad1f298..92ce4f2db9 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1680,7 +1680,6 @@ issues.timetracker_timer_stop = Stop timer
 issues.timetracker_timer_discard = Discard timer
 issues.timetracker_timer_manually_add = Add Time
 
-issues.time_estimate_placeholder = 1h 2m
 issues.time_estimate_set = Set estimated time
 issues.time_estimate_display = Estimate: %s
 issues.change_time_estimate_at = changed time estimate to <b>%s</b> %s
@@ -2633,6 +2632,7 @@ release.new_release = New Release
 release.draft = Draft
 release.prerelease = Pre-Release
 release.stable = Stable
+release.latest = Latest
 release.compare = Compare
 release.edit = edit
 release.ahead.commits = <strong>%d</strong> commits
diff --git a/package-lock.json b/package-lock.json
index 53bd5bc4f1..8755cfe06f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,7 +30,7 @@
         "esbuild-loader": "4.2.2",
         "escape-goat": "4.0.0",
         "fast-glob": "3.3.2",
-        "htmx.org": "2.0.3",
+        "htmx.org": "2.0.4",
         "idiomorph": "0.3.0",
         "jquery": "3.7.1",
         "katex": "0.16.11",
@@ -67,6 +67,7 @@
       "devDependencies": {
         "@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
         "@playwright/test": "1.49.0",
+        "@silverwind/vue-tsc": "2.1.13",
         "@stoplight/spectral-cli": "6.14.2",
         "@stylistic/eslint-plugin-js": "2.11.0",
         "@stylistic/stylelint-plugin": "3.1.1",
@@ -111,8 +112,7 @@
         "type-fest": "4.30.0",
         "updates": "16.4.0",
         "vite-string-plugin": "1.3.4",
-        "vitest": "2.1.8",
-        "vue-tsc": "2.1.10"
+        "vitest": "2.1.8"
       },
       "engines": {
         "node": ">= 18.0.0"
@@ -3833,6 +3833,24 @@
       "hasInstallScript": true,
       "license": "Apache-2.0"
     },
+    "node_modules/@silverwind/vue-tsc": {
+      "version": "2.1.13",
+      "resolved": "https://registry.npmjs.org/@silverwind/vue-tsc/-/vue-tsc-2.1.13.tgz",
+      "integrity": "sha512-ejFxz1KZiUGAESbC+eURnjqt0N95qkU9eZU7W15wgF9zV+v2FEu3ZLduuXTC7D/Sg6lL1R/QjPfUbxbAbBQOsw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/typescript": "~2.4.11",
+        "@vue/language-core": "2.1.10",
+        "semver": "^7.5.4"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.0"
+      }
+    },
     "node_modules/@silverwind/vue3-calendar-heatmap": {
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/@silverwind/vue3-calendar-heatmap/-/vue3-calendar-heatmap-2.0.6.tgz",
@@ -5335,30 +5353,30 @@
       }
     },
     "node_modules/@volar/language-core": {
-      "version": "2.4.10",
-      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.10.tgz",
-      "integrity": "sha512-hG3Z13+nJmGaT+fnQzAkS0hjJRa2FCeqZt6Bd+oGNhUkQ+mTFsDETg5rqUTxyzIh5pSOGY7FHCWUS8G82AzLCA==",
+      "version": "2.4.11",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.11.tgz",
+      "integrity": "sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@volar/source-map": "2.4.10"
+        "@volar/source-map": "2.4.11"
       }
     },
     "node_modules/@volar/source-map": {
-      "version": "2.4.10",
-      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.10.tgz",
-      "integrity": "sha512-OCV+b5ihV0RF3A7vEvNyHPi4G4kFa6ukPmyVocmqm5QzOd8r5yAtiNvaPEjl8dNvgC/lj4JPryeeHLdXd62rWA==",
+      "version": "2.4.11",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.11.tgz",
+      "integrity": "sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==",
       "dev": true,
       "license": "MIT"
     },
     "node_modules/@volar/typescript": {
-      "version": "2.4.10",
-      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.10.tgz",
-      "integrity": "sha512-F8ZtBMhSXyYKuBfGpYwqA5rsONnOwAVvjyE7KPYJ7wgZqo2roASqNWUnianOomJX5u1cxeRooHV59N0PhvEOgw==",
+      "version": "2.4.11",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.11.tgz",
+      "integrity": "sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@volar/language-core": "2.4.10",
+        "@volar/language-core": "2.4.11",
         "path-browserify": "^1.0.1",
         "vscode-uri": "^3.0.8"
       }
@@ -10557,9 +10575,9 @@
       }
     },
     "node_modules/htmx.org": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.3.tgz",
-      "integrity": "sha512-AeoJUAjkCVVajbfKX+3sVQBTCt8Ct4lif1T+z/tptTXo8+8yyq3QIMQQe/IT+R8ssfrO1I0DeX4CAronzCL6oA==",
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.4.tgz",
+      "integrity": "sha512-HLxMCdfXDOJirs3vBZl/ZLoY+c7PfM4Ahr2Ad4YXh6d22T5ltbTXFFkpx9Tgb2vvmWFMbIc3LqN2ToNkZJvyYQ==",
       "license": "0BSD"
     },
     "node_modules/iconv-lite": {
@@ -15780,24 +15798,6 @@
         }
       }
     },
-    "node_modules/vue-tsc": {
-      "version": "2.1.10",
-      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.10.tgz",
-      "integrity": "sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@volar/typescript": "~2.4.8",
-        "@vue/language-core": "2.1.10",
-        "semver": "^7.5.4"
-      },
-      "bin": {
-        "vue-tsc": "bin/vue-tsc.js"
-      },
-      "peerDependencies": {
-        "typescript": ">=5.0.0"
-      }
-    },
     "node_modules/watchpack": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
diff --git a/package.json b/package.json
index 3a81e64822..61e65c1f43 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
     "esbuild-loader": "4.2.2",
     "escape-goat": "4.0.0",
     "fast-glob": "3.3.2",
-    "htmx.org": "2.0.3",
+    "htmx.org": "2.0.4",
     "idiomorph": "0.3.0",
     "jquery": "3.7.1",
     "katex": "0.16.11",
@@ -66,6 +66,7 @@
   "devDependencies": {
     "@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
     "@playwright/test": "1.49.0",
+    "@silverwind/vue-tsc": "2.1.13",
     "@stoplight/spectral-cli": "6.14.2",
     "@stylistic/eslint-plugin-js": "2.11.0",
     "@stylistic/stylelint-plugin": "3.1.1",
@@ -110,8 +111,7 @@
     "type-fest": "4.30.0",
     "updates": "16.4.0",
     "vite-string-plugin": "1.3.4",
-    "vitest": "2.1.8",
-    "vue-tsc": "2.1.10"
+    "vitest": "2.1.8"
   },
   "browserslist": [
     "defaults"
diff --git a/routers/api/actions/runner/main_test.go b/routers/api/actions/runner/main_test.go
new file mode 100644
index 0000000000..1e80a4f5ca
--- /dev/null
+++ b/routers/api/actions/runner/main_test.go
@@ -0,0 +1,14 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package runner
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/unittest"
+)
+
+func TestMain(m *testing.M) {
+	unittest.MainTest(m)
+}
diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go
index ff6ec5bd54..539be8d889 100644
--- a/routers/api/actions/runner/utils.go
+++ b/routers/api/actions/runner/utils.go
@@ -162,28 +162,56 @@ func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[str
 		return nil, fmt.Errorf("FindRunJobs: %w", err)
 	}
 
-	ret := make(map[string]*runnerv1.TaskNeed, len(needs))
+	jobIDJobs := make(map[string][]*actions_model.ActionRunJob)
 	for _, job := range jobs {
-		if !needs.Contains(job.JobID) {
+		jobIDJobs[job.JobID] = append(jobIDJobs[job.JobID], job)
+	}
+
+	ret := make(map[string]*runnerv1.TaskNeed, len(needs))
+	for jobID, jobsWithSameID := range jobIDJobs {
+		if !needs.Contains(jobID) {
 			continue
 		}
-		if job.TaskID == 0 || !job.Status.IsDone() {
-			// it shouldn't happen, or the job has been rerun
-			continue
+		var jobOutputs map[string]string
+		for _, job := range jobsWithSameID {
+			if job.TaskID == 0 || !job.Status.IsDone() {
+				// it shouldn't happen, or the job has been rerun
+				continue
+			}
+			got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID)
+			if err != nil {
+				return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err)
+			}
+			outputs := make(map[string]string, len(got))
+			for _, v := range got {
+				outputs[v.OutputKey] = v.OutputValue
+			}
+			if len(jobOutputs) == 0 {
+				jobOutputs = outputs
+			} else {
+				jobOutputs = mergeTwoOutputs(outputs, jobOutputs)
+			}
 		}
-		outputs := make(map[string]string)
-		got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID)
-		if err != nil {
-			return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err)
-		}
-		for _, v := range got {
-			outputs[v.OutputKey] = v.OutputValue
-		}
-		ret[job.JobID] = &runnerv1.TaskNeed{
-			Outputs: outputs,
-			Result:  runnerv1.Result(job.Status),
+		ret[jobID] = &runnerv1.TaskNeed{
+			Outputs: jobOutputs,
+			Result:  runnerv1.Result(actions_model.AggregateJobStatus(jobsWithSameID)),
 		}
 	}
 
 	return ret, nil
 }
+
+// mergeTwoOutputs merges two outputs from two different ActionRunJobs
+// Values with the same output name may be overridden. The user should ensure the output names are unique.
+// See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#using-job-outputs-in-a-matrix-job
+func mergeTwoOutputs(o1, o2 map[string]string) map[string]string {
+	ret := make(map[string]string, len(o1))
+	for k1, v1 := range o1 {
+		if len(v1) > 0 {
+			ret[k1] = v1
+		} else {
+			ret[k1] = o2[k1]
+		}
+	}
+	return ret
+}
diff --git a/routers/api/actions/runner/utils_test.go b/routers/api/actions/runner/utils_test.go
new file mode 100644
index 0000000000..d7a6f84550
--- /dev/null
+++ b/routers/api/actions/runner/utils_test.go
@@ -0,0 +1,28 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package runner
+
+import (
+	"context"
+	"testing"
+
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/models/unittest"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_findTaskNeeds(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51})
+
+	ret, err := findTaskNeeds(context.Background(), task)
+	assert.NoError(t, err)
+	assert.Len(t, ret, 1)
+	assert.Contains(t, ret, "job1")
+	assert.Len(t, ret["job1"].Outputs, 2)
+	assert.Equal(t, "abc", ret["job1"].Outputs["output_a"])
+	assert.Equal(t, "bbb", ret["job1"].Outputs["output_b"])
+}
diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go
index 712c71a682..ae7502c661 100644
--- a/routers/api/v1/repo/issue_dependency.go
+++ b/routers/api/v1/repo/issue_dependency.go
@@ -338,7 +338,7 @@ func GetIssueBlocks(ctx *context.APIContext) {
 	}
 
 	skip := (page - 1) * limit
-	max := page * limit
+	maxNum := page * limit
 
 	deps, err := issue.BlockingDependencies(ctx)
 	if err != nil {
@@ -352,7 +352,7 @@ func GetIssueBlocks(ctx *context.APIContext) {
 	repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
 
 	for i, depMeta := range deps {
-		if i < skip || i >= max {
+		if i < skip || i >= maxNum {
 			continue
 		}
 
diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go
index c7065c1d9d..f9906ed250 100644
--- a/routers/api/v1/repo/wiki.go
+++ b/routers/api/v1/repo/wiki.go
@@ -308,7 +308,7 @@ func ListWikiPages(ctx *context.APIContext) {
 	}
 
 	skip := (page - 1) * limit
-	max := page * limit
+	maxNum := page * limit
 
 	entries, err := commit.ListEntries()
 	if err != nil {
@@ -317,7 +317,7 @@ func ListWikiPages(ctx *context.APIContext) {
 	}
 	pages := make([]*api.WikiPageMetaData, 0, len(entries))
 	for i, entry := range entries {
-		if i < skip || i >= max || !entry.IsRegular() {
+		if i < skip || i >= maxNum || !entry.IsRegular() {
 			continue
 		}
 		c, err := wikiRepo.GetCommitByPath(entry.Name())
diff --git a/routers/private/hook_post_receive_test.go b/routers/private/hook_post_receive_test.go
index 658557d3cf..a089739d15 100644
--- a/routers/private/hook_post_receive_test.go
+++ b/routers/private/hook_post_receive_test.go
@@ -39,7 +39,7 @@ func TestHandlePullRequestMerging(t *testing.T) {
 	}, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, []*repo_module.PushUpdateOptions{
 		{NewCommitID: "01234567"},
 	})
-	assert.Equal(t, 0, len(resp.Body.String()))
+	assert.Empty(t, resp.Body.String())
 	pr, err = issues_model.GetPullRequestByID(db.DefaultContext, pr.ID)
 	assert.NoError(t, err)
 	assert.True(t, pr.HasMerged)
diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index 7ed37ea26b..1de1835936 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -5,6 +5,7 @@ package actions
 
 import (
 	"bytes"
+	stdCtx "context"
 	"fmt"
 	"net/http"
 	"slices"
@@ -245,7 +246,7 @@ func List(ctx *context.Context) {
 		return
 	}
 
-	if err := loadIsRefDeleted(ctx, runs); err != nil {
+	if err := loadIsRefDeleted(ctx, ctx.Repo.Repository.ID, runs); err != nil {
 		log.Error("LoadIsRefDeleted", err)
 	}
 
@@ -273,7 +274,7 @@ func List(ctx *context.Context) {
 
 // loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
 // TODO: move this function to models/actions/run_list.go but now it will result in a circular import.
-func loadIsRefDeleted(ctx *context.Context, runs actions_model.RunList) error {
+func loadIsRefDeleted(ctx stdCtx.Context, repoID int64, runs actions_model.RunList) error {
 	branches := make(container.Set[string], len(runs))
 	for _, run := range runs {
 		refName := git.RefName(run.Ref)
@@ -285,14 +286,14 @@ func loadIsRefDeleted(ctx *context.Context, runs actions_model.RunList) error {
 		return nil
 	}
 
-	branchInfos, err := git_model.GetBranches(ctx, ctx.Repo.Repository.ID, branches.Values(), false)
+	branchInfos, err := git_model.GetBranches(ctx, repoID, branches.Values(), false)
 	if err != nil {
 		return err
 	}
 	branchSet := git_model.BranchesToNamesSet(branchInfos)
 	for _, run := range runs {
 		refName := git.RefName(run.Ref)
-		if refName.IsBranch() && !branchSet.Contains(run.Ref) {
+		if refName.IsBranch() && !branchSet.Contains(refName.ShortName()) {
 			run.IsRefDeleted = true
 		}
 	}
diff --git a/routers/web/repo/actions/actions_test.go b/routers/web/repo/actions/actions_test.go
index 194704d14e..6a976ed65c 100644
--- a/routers/web/repo/actions/actions_test.go
+++ b/routers/web/repo/actions/actions_test.go
@@ -7,6 +7,10 @@ import (
 	"strings"
 	"testing"
 
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/models/db"
+	unittest "code.gitea.io/gitea/models/unittest"
+
 	act_model "github.com/nektos/act/pkg/model"
 	"github.com/stretchr/testify/assert"
 )
@@ -154,3 +158,21 @@ func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) {
 		Type:        "boolean",
 	}, workflowDispatch.Inputs[2])
 }
+
+func Test_loadIsRefDeleted(t *testing.T) {
+	unittest.PrepareTestEnv(t)
+
+	runs, total, err := db.FindAndCount[actions_model.ActionRun](db.DefaultContext,
+		actions_model.FindRunOptions{RepoID: 4, Ref: "refs/heads/test"})
+	assert.NoError(t, err)
+	assert.Len(t, runs, 1)
+	assert.EqualValues(t, 1, total)
+	for _, run := range runs {
+		assert.False(t, run.IsRefDeleted)
+	}
+
+	assert.NoError(t, loadIsRefDeleted(db.DefaultContext, 4, runs))
+	for _, run := range runs {
+		assert.True(t, run.IsRefDeleted)
+	}
+}
diff --git a/routers/web/repo/actions/main_test.go b/routers/web/repo/actions/main_test.go
new file mode 100644
index 0000000000..a82f9c6672
--- /dev/null
+++ b/routers/web/repo/actions/main_test.go
@@ -0,0 +1,14 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/unittest"
+)
+
+func TestMain(m *testing.M) {
+	unittest.MainTest(m)
+}
diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go
index 27e42a8f98..86af705617 100644
--- a/routers/web/repo/fork.go
+++ b/routers/web/repo/fork.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
+	"code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
@@ -48,7 +49,7 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository {
 	ctx.Data["repo_name"] = forkRepo.Name
 	ctx.Data["description"] = forkRepo.Description
 	ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate
-	canForkToUser := forkRepo.OwnerID != ctx.Doer.ID && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID)
+	canForkToUser := repository.CanUserForkBetweenOwners(forkRepo.OwnerID, ctx.Doer.ID) && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID)
 
 	ctx.Data["ForkRepo"] = forkRepo
 
@@ -66,7 +67,7 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository {
 
 	traverseParentRepo := forkRepo
 	for {
-		if ctx.Doer.ID == traverseParentRepo.OwnerID {
+		if !repository.CanUserForkBetweenOwners(ctx.Doer.ID, traverseParentRepo.OwnerID) {
 			canForkToUser = false
 		} else {
 			for i, org := range orgs {
@@ -162,7 +163,7 @@ func ForkPost(ctx *context.Context) {
 	var err error
 	traverseParentRepo := forkRepo
 	for {
-		if ctxUser.ID == traverseParentRepo.OwnerID {
+		if !repository.CanUserForkBetweenOwners(ctxUser.ID, traverseParentRepo.OwnerID) {
 			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
 			return
 		}
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index b81f2ea02e..958ff802d4 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -135,7 +135,7 @@ func TestNewWikiPost(t *testing.T) {
 		NewWikiPost(ctx)
 		assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
 		assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
-		assert.Equal(t, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)), content)
+		assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)))
 	}
 }
 
@@ -194,7 +194,7 @@ func TestEditWikiPost(t *testing.T) {
 		EditWikiPost(ctx)
 		assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
 		assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
-		assert.Equal(t, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)), content)
+		assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)))
 		if title != "Home" {
 			assertWikiNotExists(t, ctx.Repo.Repository, "Home")
 		}
diff --git a/services/actions/auth_test.go b/services/actions/auth_test.go
index 12db2bae56..85e7409105 100644
--- a/services/actions/auth_test.go
+++ b/services/actions/auth_test.go
@@ -17,19 +17,19 @@ import (
 func TestCreateAuthorizationToken(t *testing.T) {
 	var taskID int64 = 23
 	token, err := CreateAuthorizationToken(taskID, 1, 2)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	assert.NotEqual(t, "", token)
 	claims := jwt.MapClaims{}
 	_, err = jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (any, error) {
 		return setting.GetGeneralTokenSigningSecret(), nil
 	})
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	scp, ok := claims["scp"]
 	assert.True(t, ok, "Has scp claim in jwt token")
 	assert.Contains(t, scp, "Actions.Results:1:2")
 	taskIDClaim, ok := claims["TaskID"]
 	assert.True(t, ok, "Has TaskID claim in jwt token")
-	assert.Equal(t, float64(taskID), taskIDClaim, "Supplied taskid must match stored one")
+	assert.InDelta(t, float64(taskID), taskIDClaim, 0, "Supplied taskid must match stored one")
 	acClaim, ok := claims["ac"]
 	assert.True(t, ok, "Has ac claim in jwt token")
 	ac, ok := acClaim.(string)
@@ -43,14 +43,14 @@ func TestCreateAuthorizationToken(t *testing.T) {
 func TestParseAuthorizationToken(t *testing.T) {
 	var taskID int64 = 23
 	token, err := CreateAuthorizationToken(taskID, 1, 2)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	assert.NotEqual(t, "", token)
 	headers := http.Header{}
 	headers.Set("Authorization", "Bearer "+token)
 	rTaskID, err := ParseAuthorizationToken(&http.Request{
 		Header: headers,
 	})
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	assert.Equal(t, taskID, rTaskID)
 }
 
@@ -59,6 +59,6 @@ func TestParseAuthorizationTokenNoAuthHeader(t *testing.T) {
 	rTaskID, err := ParseAuthorizationToken(&http.Request{
 		Header: headers,
 	})
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	assert.Equal(t, int64(0), rTaskID)
 }
diff --git a/services/auth/oauth2_test.go b/services/auth/oauth2_test.go
index 75c231ff7a..b706847e8e 100644
--- a/services/auth/oauth2_test.go
+++ b/services/auth/oauth2_test.go
@@ -28,7 +28,7 @@ func TestUserIDFromToken(t *testing.T) {
 		o := OAuth2{}
 		uid := o.userIDFromToken(context.Background(), token, ds)
 		assert.Equal(t, int64(user_model.ActionsUserID), uid)
-		assert.Equal(t, ds["IsActionsToken"], true)
+		assert.Equal(t, true, ds["IsActionsToken"])
 		assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID))
 	})
 }
diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go
index dc4cb2c940..963cdba7c2 100644
--- a/services/auth/source/ldap/source.go
+++ b/services/auth/source/ldap/source.go
@@ -56,8 +56,7 @@ type Source struct {
 	UserUID               string // User Attribute listed in Group
 	SkipLocalTwoFA        bool   `json:",omitempty"` // Skip Local 2fa for users authenticated with this source
 
-	// reference to the authSource
-	authSource *auth.Source
+	authSource *auth.Source // reference to the authSource
 }
 
 // FromDB fills up a LDAPConfig from serialized format.
@@ -107,7 +106,7 @@ func (source *Source) UseTLS() bool {
 
 // ProvidesSSHKeys returns if this source provides SSH Keys
 func (source *Source) ProvidesSSHKeys() bool {
-	return len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
+	return strings.TrimSpace(source.AttributeSSHPublicKey) != ""
 }
 
 // SetAuthSource sets the related AuthSource
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index 01cb743720..020e5784dc 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -31,13 +31,13 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
 		return nil, user_model.ErrUserNotExist{Name: loginName}
 	}
 	// Fallback.
-	if len(sr.Username) == 0 {
+	if sr.Username == "" {
 		sr.Username = userName
 	}
-	if len(sr.Mail) == 0 {
+	if sr.Mail == "" {
 		sr.Mail = fmt.Sprintf("%s@localhost.local", sr.Username)
 	}
-	isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
+	isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != ""
 
 	// Update User admin flag if exist
 	if isExist, err := user_model.IsUserExist(ctx, 0, sr.Username); err != nil {
@@ -51,11 +51,11 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
 		}
 		if user != nil && !user.ProhibitLogin {
 			opts := &user_service.UpdateOptions{}
-			if len(source.AdminFilter) > 0 && user.IsAdmin != sr.IsAdmin {
+			if source.AdminFilter != "" && user.IsAdmin != sr.IsAdmin {
 				// Change existing admin flag only if AdminFilter option is set
 				opts.IsAdmin = optional.Some(sr.IsAdmin)
 			}
-			if !sr.IsAdmin && len(source.RestrictedFilter) > 0 && user.IsRestricted != sr.IsRestricted {
+			if !sr.IsAdmin && source.RestrictedFilter != "" && user.IsRestricted != sr.IsRestricted {
 				// Change existing restricted flag only if RestrictedFilter option is set
 				opts.IsRestricted = optional.Some(sr.IsRestricted)
 			}
@@ -99,7 +99,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
 				return user, err
 			}
 		}
-		if len(source.AttributeAvatar) > 0 {
+		if source.AttributeAvatar != "" {
 			if err := user_service.UploadAvatar(ctx, user, sr.Avatar); err != nil {
 				return user, err
 			}
diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go
index b20c90e791..fa2c45ce4a 100644
--- a/services/auth/source/ldap/source_search.go
+++ b/services/auth/source/ldap/source_search.go
@@ -147,7 +147,7 @@ func bindUser(l *ldap.Conn, userDN, passwd string) error {
 }
 
 func checkAdmin(l *ldap.Conn, ls *Source, userDN string) bool {
-	if len(ls.AdminFilter) == 0 {
+	if ls.AdminFilter == "" {
 		return false
 	}
 	log.Trace("Checking admin with filter %s and base %s", ls.AdminFilter, userDN)
@@ -169,7 +169,7 @@ func checkAdmin(l *ldap.Conn, ls *Source, userDN string) bool {
 }
 
 func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool {
-	if len(ls.RestrictedFilter) == 0 {
+	if ls.RestrictedFilter == "" {
 		return false
 	}
 	if ls.RestrictedFilter == "*" {
@@ -250,8 +250,17 @@ func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string {
 
 // SearchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter
 func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult {
+	if MockedSearchEntry != nil {
+		return MockedSearchEntry(source, name, passwd, directBind)
+	}
+	return realSearchEntry(source, name, passwd, directBind)
+}
+
+var MockedSearchEntry func(source *Source, name, passwd string, directBind bool) *SearchResult
+
+func realSearchEntry(source *Source, name, passwd string, directBind bool) *SearchResult {
 	// See https://tools.ietf.org/search/rfc4513#section-5.1.2
-	if len(passwd) == 0 {
+	if passwd == "" {
 		log.Debug("Auth. failed for %s, password cannot be empty", name)
 		return nil
 	}
@@ -323,17 +332,17 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
 		return nil
 	}
 
-	isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
-	isAtributeAvatarSet := len(strings.TrimSpace(source.AttributeAvatar)) > 0
+	isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != ""
+	isAttributeAvatarSet := strings.TrimSpace(source.AttributeAvatar) != ""
 
 	attribs := []string{source.AttributeUsername, source.AttributeName, source.AttributeSurname, source.AttributeMail}
-	if len(strings.TrimSpace(source.UserUID)) > 0 {
+	if strings.TrimSpace(source.UserUID) != "" {
 		attribs = append(attribs, source.UserUID)
 	}
 	if isAttributeSSHPublicKeySet {
 		attribs = append(attribs, source.AttributeSSHPublicKey)
 	}
-	if isAtributeAvatarSet {
+	if isAttributeAvatarSet {
 		attribs = append(attribs, source.AttributeAvatar)
 	}
 
@@ -375,7 +384,7 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
 		isRestricted = checkRestricted(l, source, userDN)
 	}
 
-	if isAtributeAvatarSet {
+	if isAttributeAvatarSet {
 		Avatar = sr.Entries[0].GetRawAttributeValue(source.AttributeAvatar)
 	}
 
@@ -440,14 +449,14 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) {
 
 	userFilter := fmt.Sprintf(source.Filter, "*")
 
-	isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
-	isAtributeAvatarSet := len(strings.TrimSpace(source.AttributeAvatar)) > 0
+	isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != ""
+	isAttributeAvatarSet := strings.TrimSpace(source.AttributeAvatar) != ""
 
 	attribs := []string{source.AttributeUsername, source.AttributeName, source.AttributeSurname, source.AttributeMail, source.UserUID}
 	if isAttributeSSHPublicKeySet {
 		attribs = append(attribs, source.AttributeSSHPublicKey)
 	}
-	if isAtributeAvatarSet {
+	if isAttributeAvatarSet {
 		attribs = append(attribs, source.AttributeAvatar)
 	}
 
@@ -503,7 +512,7 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) {
 			user.SSHPublicKey = v.GetAttributeValues(source.AttributeSSHPublicKey)
 		}
 
-		if isAtributeAvatarSet {
+		if isAttributeAvatarSet {
 			user.Avatar = v.GetRawAttributeValue(source.AttributeAvatar)
 		}
 
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index a6d6d2a0f2..e817bf1fa9 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -25,7 +25,7 @@ import (
 func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 	log.Trace("Doing: SyncExternalUsers[%s]", source.authSource.Name)
 
-	isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
+	isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != ""
 	var sshKeysNeedUpdate bool
 
 	// Find all users with this login type - FIXME: Should this be an iterator?
@@ -86,26 +86,26 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 			return db.ErrCancelledf("During update of %s before completed update of users", source.authSource.Name)
 		default:
 		}
-		if len(su.Username) == 0 && len(su.Mail) == 0 {
+		if su.Username == "" && su.Mail == "" {
 			continue
 		}
 
 		var usr *user_model.User
-		if len(su.Username) > 0 {
+		if su.Username != "" {
 			usr = usernameUsers[su.LowerName]
 		}
-		if usr == nil && len(su.Mail) > 0 {
+		if usr == nil && su.Mail != "" {
 			usr = mailUsers[strings.ToLower(su.Mail)]
 		}
 
 		if usr != nil {
 			keepActiveUsers.Add(usr.ID)
-		} else if len(su.Username) == 0 {
+		} else if su.Username == "" {
 			// we cannot create the user if su.Username is empty
 			continue
 		}
 
-		if len(su.Mail) == 0 {
+		if su.Mail == "" {
 			su.Mail = fmt.Sprintf("%s@localhost.local", su.Username)
 		}
 
@@ -141,7 +141,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 				}
 			}
 
-			if err == nil && len(source.AttributeAvatar) > 0 {
+			if err == nil && source.AttributeAvatar != "" {
 				_ = user_service.UploadAvatar(ctx, usr, su.Avatar)
 			}
 		} else if updateExisting {
@@ -151,8 +151,8 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 			}
 
 			// Check if user data has changed
-			if (len(source.AdminFilter) > 0 && usr.IsAdmin != su.IsAdmin) ||
-				(len(source.RestrictedFilter) > 0 && usr.IsRestricted != su.IsRestricted) ||
+			if (source.AdminFilter != "" && usr.IsAdmin != su.IsAdmin) ||
+				(source.RestrictedFilter != "" && usr.IsRestricted != su.IsRestricted) ||
 				!strings.EqualFold(usr.Email, su.Mail) ||
 				usr.FullName != fullName ||
 				!usr.IsActive {
@@ -180,7 +180,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 			}
 
 			if usr.IsUploadAvatarChanged(su.Avatar) {
-				if err == nil && len(source.AttributeAvatar) > 0 {
+				if err == nil && source.AttributeAvatar != "" {
 					_ = user_service.UploadAvatar(ctx, usr, su.Avatar)
 				}
 			}
diff --git a/services/auth/source/ldap/util.go b/services/auth/source/ldap/util.go
index bd11e2d119..05ae32c0fd 100644
--- a/services/auth/source/ldap/util.go
+++ b/services/auth/source/ldap/util.go
@@ -6,11 +6,11 @@ package ldap
 // composeFullName composes a firstname surname or username
 func composeFullName(firstname, surname, username string) string {
 	switch {
-	case len(firstname) == 0 && len(surname) == 0:
+	case firstname == "" && surname == "":
 		return username
-	case len(firstname) == 0:
+	case firstname == "":
 		return surname
-	case len(surname) == 0:
+	case surname == "":
 		return firstname
 	default:
 		return firstname + " " + surname
diff --git a/services/auth/source/oauth2/source_sync_test.go b/services/auth/source/oauth2/source_sync_test.go
index 25408e8727..893ed62502 100644
--- a/services/auth/source/oauth2/source_sync_test.go
+++ b/services/auth/source/oauth2/source_sync_test.go
@@ -64,8 +64,8 @@ func TestSource(t *testing.T) {
 			ok, err := user_model.GetExternalLogin(context.Background(), e)
 			assert.NoError(t, err)
 			assert.True(t, ok)
-			assert.Equal(t, e.RefreshToken, "refresh")
-			assert.Equal(t, e.AccessToken, "token")
+			assert.Equal(t, "refresh", e.RefreshToken)
+			assert.Equal(t, "token", e.AccessToken)
 
 			u, err := user_model.GetUserByID(context.Background(), user.ID)
 			assert.NoError(t, err)
@@ -89,8 +89,8 @@ func TestSource(t *testing.T) {
 			ok, err := user_model.GetExternalLogin(context.Background(), e)
 			assert.NoError(t, err)
 			assert.True(t, ok)
-			assert.Equal(t, e.RefreshToken, "")
-			assert.Equal(t, e.AccessToken, "")
+			assert.Equal(t, "", e.RefreshToken)
+			assert.Equal(t, "", e.AccessToken)
 
 			u, err := user_model.GetUserByID(context.Background(), user.ID)
 			assert.NoError(t, err)
diff --git a/services/convert/pull_review_test.go b/services/convert/pull_review_test.go
index 6886950280..a1296fafd4 100644
--- a/services/convert/pull_review_test.go
+++ b/services/convert/pull_review_test.go
@@ -40,7 +40,7 @@ func Test_ToPullReview(t *testing.T) {
 		user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 		prList, err := ToPullReviewList(db.DefaultContext, reviewList, user4)
 		assert.NoError(t, err)
-		assert.Len(t, prList, 0)
+		assert.Empty(t, prList)
 	})
 
 	t.Run("Admin User", func(t *testing.T) {
diff --git a/services/cron/tasks_test.go b/services/cron/tasks_test.go
index 979371a022..ab22403ede 100644
--- a/services/cron/tasks_test.go
+++ b/services/cron/tasks_test.go
@@ -12,7 +12,7 @@ import (
 )
 
 func TestAddTaskToScheduler(t *testing.T) {
-	assert.Len(t, scheduler.Jobs(), 0)
+	assert.Empty(t, scheduler.Jobs())
 	defer scheduler.Clear()
 
 	// no seconds
diff --git a/services/feed/feed_test.go b/services/feed/feed_test.go
index 6f1cb9a969..1e4d029e18 100644
--- a/services/feed/feed_test.go
+++ b/services/feed/feed_test.go
@@ -41,7 +41,7 @@ func TestGetFeeds(t *testing.T) {
 		OnlyPerformedBy: false,
 	})
 	assert.NoError(t, err)
-	assert.Len(t, actions, 0)
+	assert.Empty(t, actions)
 	assert.Equal(t, int64(0), count)
 }
 
@@ -57,7 +57,7 @@ func TestGetFeedsForRepos(t *testing.T) {
 		IncludePrivate: true,
 	})
 	assert.NoError(t, err)
-	assert.Len(t, actions, 0)
+	assert.Empty(t, actions)
 	assert.Equal(t, int64(0), count)
 
 	// public repo & no login
@@ -119,7 +119,7 @@ func TestGetFeeds2(t *testing.T) {
 		IncludeDeleted:  true,
 	})
 	assert.NoError(t, err)
-	assert.Len(t, actions, 0)
+	assert.Empty(t, actions)
 	assert.Equal(t, int64(0), count)
 }
 
diff --git a/services/feed/notifier.go b/services/feed/notifier.go
index a8820aeb77..d941027c35 100644
--- a/services/feed/notifier.go
+++ b/services/feed/notifier.go
@@ -417,6 +417,12 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model
 }
 
 func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) {
+	// ignore pull sync message for pull requests refs
+	// TODO: it's better to have a UI to let users chose
+	if refFullName.IsPull() {
+		return
+	}
+
 	if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
 		ActUserID: repo.OwnerID,
 		ActUser:   repo.MustOwner(ctx),
@@ -431,6 +437,12 @@ func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.Use
 }
 
 func (a *actionNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) {
+	// ignore pull sync message for pull requests refs
+	// TODO: it's better to have a UI to let users chose
+	if refFullName.IsPull() {
+		return
+	}
+
 	if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
 		ActUserID: repo.OwnerID,
 		ActUser:   repo.MustOwner(ctx),
diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go
index adcac355a7..2351c5da87 100644
--- a/services/gitdiff/gitdiff_test.go
+++ b/services/gitdiff/gitdiff_test.go
@@ -5,7 +5,6 @@
 package gitdiff
 
 import (
-	"fmt"
 	"strconv"
 	"strings"
 	"testing"
@@ -643,9 +642,9 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
 				MaxFiles:           setting.Git.MaxGitDiffFiles,
 				WhitespaceBehavior: behavior,
 			})
-		assert.NoError(t, err, fmt.Sprintf("Error when diff with %s", behavior))
+		assert.NoError(t, err, "Error when diff with %s", behavior)
 		for _, f := range diffs.Files {
-			assert.True(t, len(f.Sections) > 0, fmt.Sprintf("%s should have sections", f.Name))
+			assert.NotEmpty(t, f.Sections, "%s should have sections", f.Name)
 		}
 	}
 }
diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go
index 0b9eeaed54..eccfc4def1 100644
--- a/services/migrations/gitlab_test.go
+++ b/services/migrations/gitlab_test.go
@@ -50,7 +50,7 @@ func TestGitlabDownloadRepo(t *testing.T) {
 
 	topics, err := downloader.GetTopics()
 	assert.NoError(t, err)
-	assert.True(t, len(topics) == 2)
+	assert.Len(t, topics, 2)
 	assert.EqualValues(t, []string{"migration", "test"}, topics)
 
 	milestones, err := downloader.GetMilestones()
diff --git a/services/org/team_test.go b/services/org/team_test.go
index 58b8e0803c..98addac8f8 100644
--- a/services/org/team_test.go
+++ b/services/org/team_test.go
@@ -121,7 +121,7 @@ func TestDeleteTeam(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
 	accessMode, err := access_model.AccessLevel(db.DefaultContext, user, repo)
 	assert.NoError(t, err)
-	assert.True(t, accessMode < perm.AccessModeWrite)
+	assert.Less(t, accessMode, perm.AccessModeWrite)
 }
 
 func TestAddTeamMember(t *testing.T) {
diff --git a/services/pull/reviewer_test.go b/services/pull/reviewer_test.go
index 1ff373bafb..b106e2e89f 100644
--- a/services/pull/reviewer_test.go
+++ b/services/pull/reviewer_test.go
@@ -30,7 +30,7 @@ func TestRepoGetReviewers(t *testing.T) {
 	// should not include doer and remove the poster
 	reviewers, err = pull_service.GetReviewers(ctx, repo1, 11, 2)
 	assert.NoError(t, err)
-	assert.Len(t, reviewers, 0)
+	assert.Empty(t, reviewers)
 
 	// should not include PR poster, if PR poster would be otherwise eligible
 	reviewers, err = pull_service.GetReviewers(ctx, repo1, 11, 4)
@@ -43,7 +43,7 @@ func TestRepoGetReviewers(t *testing.T) {
 	reviewers, err = pull_service.GetReviewers(ctx, repo2, 2, 4)
 	assert.NoError(t, err)
 	assert.Len(t, reviewers, 1)
-	assert.EqualValues(t, reviewers[0].ID, 2)
+	assert.EqualValues(t, 2, reviewers[0].ID)
 
 	// test private org repo
 	repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
diff --git a/services/release/release_test.go b/services/release/release_test.go
index 3d0681f1e1..95a54832b9 100644
--- a/services/release/release_test.go
+++ b/services/release/release_test.go
@@ -228,7 +228,7 @@ func TestRelease_Update(t *testing.T) {
 		IsTag:        false,
 	}
 	assert.NoError(t, CreateRelease(gitRepo, release, nil, ""))
-	assert.Greater(t, release.ID, int64(0))
+	assert.Positive(t, release.ID)
 
 	release.IsDraft = false
 	tagName := release.TagName
diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go
index 2ab18edf49..1d0c6e513d 100644
--- a/services/repository/archiver/archiver_test.go
+++ b/services/repository/archiver/archiver_test.go
@@ -4,7 +4,6 @@
 package archiver
 
 import (
-	"errors"
 	"testing"
 	"time"
 
@@ -121,7 +120,7 @@ func TestArchive_Basic(t *testing.T) {
 	// It's fine to go ahead and set it to nil now.
 
 	assert.Equal(t, zipReq, zipReq2)
-	assert.False(t, zipReq == zipReq2)
+	assert.NotSame(t, zipReq, zipReq2)
 
 	// Same commit, different compression formats should have different names.
 	// Ideally, the extension would match what we originally requested.
@@ -131,5 +130,5 @@ func TestArchive_Basic(t *testing.T) {
 
 func TestErrUnknownArchiveFormat(t *testing.T) {
 	err := ErrUnknownArchiveFormat{RequestFormat: "master"}
-	assert.True(t, errors.Is(err, ErrUnknownArchiveFormat{}))
+	assert.ErrorIs(t, err, ErrUnknownArchiveFormat{})
 }
diff --git a/services/repository/license_test.go b/services/repository/license_test.go
index 39e9738145..9d3e0f36e3 100644
--- a/services/repository/license_test.go
+++ b/services/repository/license_test.go
@@ -65,7 +65,7 @@ func Test_detectLicense(t *testing.T) {
 	result, err := detectLicense(strings.NewReader(tests[2].arg + tests[3].arg + tests[4].arg))
 	assert.NoError(t, err)
 	t.Run("multiple licenses test", func(t *testing.T) {
-		assert.Equal(t, 3, len(result))
+		assert.Len(t, result, 3)
 		assert.Contains(t, result, tests[2].want[0])
 		assert.Contains(t, result, tests[3].want[0])
 		assert.Contains(t, result, tests[4].want[0])
diff --git a/services/repository/transfer_test.go b/services/repository/transfer_test.go
index 67799eddcc..0401701ba5 100644
--- a/services/repository/transfer_test.go
+++ b/services/repository/transfer_test.go
@@ -103,7 +103,7 @@ func TestRepositoryTransfer(t *testing.T) {
 	assert.NoError(t, models.CreatePendingRepositoryTransfer(db.DefaultContext, doer, user2, repo.ID, nil))
 
 	transfer, err = models.GetPendingRepositoryTransfer(db.DefaultContext, repo)
-	assert.Nil(t, err)
+	assert.NoError(t, err)
 	assert.NoError(t, transfer.LoadAttributes(db.DefaultContext))
 	assert.Equal(t, "user2", transfer.Recipient.Name)
 
diff --git a/services/webhook/packagist_test.go b/services/webhook/packagist_test.go
index e9b0695baa..f47807fa6e 100644
--- a/services/webhook/packagist_test.go
+++ b/services/webhook/packagist_test.go
@@ -25,7 +25,7 @@ func TestPackagistPayload(t *testing.T) {
 
 		pl, err := pc.Create(p)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 	})
 
 	t.Run("Delete", func(t *testing.T) {
@@ -33,7 +33,7 @@ func TestPackagistPayload(t *testing.T) {
 
 		pl, err := pc.Delete(p)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 	})
 
 	t.Run("Fork", func(t *testing.T) {
@@ -41,7 +41,7 @@ func TestPackagistPayload(t *testing.T) {
 
 		pl, err := pc.Fork(p)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 	})
 
 	t.Run("Push", func(t *testing.T) {
@@ -59,12 +59,12 @@ func TestPackagistPayload(t *testing.T) {
 		p.Action = api.HookIssueOpened
 		pl, err := pc.Issue(p)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 
 		p.Action = api.HookIssueClosed
 		pl, err = pc.Issue(p)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 	})
 
 	t.Run("IssueComment", func(t *testing.T) {
@@ -72,7 +72,7 @@ func TestPackagistPayload(t *testing.T) {
 
 		pl, err := pc.IssueComment(p)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 	})
 
 	t.Run("PullRequest", func(t *testing.T) {
@@ -80,7 +80,7 @@ func TestPackagistPayload(t *testing.T) {
 
 		pl, err := pc.PullRequest(p)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 	})
 
 	t.Run("PullRequestComment", func(t *testing.T) {
@@ -88,7 +88,7 @@ func TestPackagistPayload(t *testing.T) {
 
 		pl, err := pc.IssueComment(p)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 	})
 
 	t.Run("Review", func(t *testing.T) {
@@ -97,7 +97,7 @@ func TestPackagistPayload(t *testing.T) {
 
 		pl, err := pc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 	})
 
 	t.Run("Repository", func(t *testing.T) {
@@ -105,7 +105,7 @@ func TestPackagistPayload(t *testing.T) {
 
 		pl, err := pc.Repository(p)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 	})
 
 	t.Run("Package", func(t *testing.T) {
@@ -113,7 +113,7 @@ func TestPackagistPayload(t *testing.T) {
 
 		pl, err := pc.Package(p)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 	})
 
 	t.Run("Wiki", func(t *testing.T) {
@@ -122,17 +122,17 @@ func TestPackagistPayload(t *testing.T) {
 		p.Action = api.HookWikiCreated
 		pl, err := pc.Wiki(p)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 
 		p.Action = api.HookWikiEdited
 		pl, err = pc.Wiki(p)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 
 		p.Action = api.HookWikiDeleted
 		pl, err = pc.Wiki(p)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 	})
 
 	t.Run("Release", func(t *testing.T) {
@@ -140,7 +140,7 @@ func TestPackagistPayload(t *testing.T) {
 
 		pl, err := pc.Release(p)
 		require.NoError(t, err)
-		require.Equal(t, pl, PackagistPayload{})
+		require.Equal(t, PackagistPayload{}, pl)
 	})
 }
 
diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go
index 5f5c146232..63cbce1771 100644
--- a/services/webhook/webhook_test.go
+++ b/services/webhook/webhook_test.go
@@ -21,11 +21,11 @@ func TestWebhook_GetSlackHook(t *testing.T) {
 		Meta: `{"channel": "foo", "username": "username", "color": "blue"}`,
 	}
 	slackHook := GetSlackHook(w)
-	assert.Equal(t, *slackHook, SlackMeta{
+	assert.Equal(t, SlackMeta{
 		Channel:  "foo",
 		Username: "username",
 		Color:    "blue",
-	})
+	}, *slackHook)
 }
 
 func TestPrepareWebhooks(t *testing.T) {
diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl
index 7361df99ea..80519361fd 100644
--- a/templates/org/header.tmpl
+++ b/templates/org/header.tmpl
@@ -1,6 +1,6 @@
 <div class="ui container tw-flex">
 	{{ctx.AvatarUtils.Avatar .Org 100 "org-avatar"}}
-	<div id="org-info" class="tw-flex tw-flex-col">
+	<div id="org-info" class="tw-flex tw-flex-col tw-flex-1 tw-break-anywhere">
 		<div class="ui header">
 			{{.Org.DisplayName}}
 			<span class="org-visibility">
@@ -18,7 +18,7 @@
 				{{end}}
 			</span>
 		</div>
-		{{if .RenderedDescription}}<div class="render-content markup">{{.RenderedDescription}}</div>{{end}}
+		{{if .RenderedDescription}}<div class="render-content markup tw-break-anywhere">{{.RenderedDescription}}</div>{{end}}
 		<div class="text light meta tw-mt-1">
 			{{if .Org.Location}}<div class="flex-text-block">{{svg "octicon-location"}} <span>{{.Org.Location}}</span></div>{{end}}
 			{{if .Org.Website}}<div class="flex-text-block">{{svg "octicon-link"}} <a class="muted" target="_blank" rel="noopener noreferrer me" href="{{.Org.Website}}">{{.Org.Website}}</a></div>{{end}}
diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl
index aaed25bfbd..207774bfef 100644
--- a/templates/package/content/container.tmpl
+++ b/templates/package/content/container.tmpl
@@ -36,11 +36,13 @@
 				</thead>
 				<tbody>
 					{{range .PackageDescriptor.Metadata.Manifests}}
-					<tr>
-						<td><a href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .Digest}}">{{.Digest}}</a></td>
-						<td>{{.Platform}}</td>
-						<td>{{FileSize .Size}}</td>
-					</tr>
+						{{if ne .Platform "unknown/unknown"}}
+						<tr>
+							<td><a href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .Digest}}">{{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}}</a></td>
+							<td>{{.Platform}}</td>
+							<td>{{FileSize .Size}}</td>
+						</tr>
+						{{end}}
 					{{end}}
 				</tbody>
 			</table>
diff --git a/templates/repo/actions/status.tmpl b/templates/repo/actions/status.tmpl
index a0e02cf8d7..64c2543302 100644
--- a/templates/repo/actions/status.tmpl
+++ b/templates/repo/actions/status.tmpl
@@ -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>
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 4e6d375b51..d73b7470bc 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -18,7 +18,7 @@
 
 		{{$treeNamesLen := len .TreeNames}}
 		{{$isTreePathRoot := eq $treeNamesLen 0}}
-		{{$showSidebar := $isTreePathRoot}}
+		{{$showSidebar := and $isTreePathRoot (not .HideRepoInfo) (not .IsBlame)}}
 		<div class="{{Iif $showSidebar "repo-grid-filelist-sidebar" "repo-grid-filelist-only"}}">
 			<div class="repo-home-filelist">
 				{{template "repo/sub_menu" .}}
@@ -130,8 +130,8 @@
 			</div>
 
 			{{if $showSidebar}}
-				<div class="repo-home-sidebar-top">{{template "repo/home_sidebar_top" .}}</div>
-				<div class="repo-home-sidebar-bottom">{{template "repo/home_sidebar_bottom" .}}</div>
+				{{template "repo/home_sidebar_top" .}}
+				{{template "repo/home_sidebar_bottom" .}}
 			{{end}}
 		</div>
 	</div>
diff --git a/templates/repo/home_sidebar_bottom.tmpl b/templates/repo/home_sidebar_bottom.tmpl
index 57b4a95ddc..f780dc122d 100644
--- a/templates/repo/home_sidebar_bottom.tmpl
+++ b/templates/repo/home_sidebar_bottom.tmpl
@@ -1,59 +1,61 @@
-<div class="flex-list">
-	{{if .LatestRelease}}
-	<div class="flex-item">
-		<div class="flex-item-main">
-			<div class="flex-item-title">
-				<a class="item muted" href="{{.Link}}/releases">
-					{{ctx.Locale.Tr "repo.releases"}}
-					<span class="ui small label">{{.NumReleases}}</span>
-				</a>
-			</div>
-			<div class="flex-item">
-				<div class="flex-item-icon">
-					{{svg "octicon-tag" 16}}
+<div class="repo-home-sidebar-bottom">
+	<div class="flex-list">
+		{{if .LatestRelease}}
+		<div class="flex-item">
+			<div class="flex-item-main">
+				<div class="flex-item-title">
+					<a class="item muted" href="{{.Link}}/releases">
+						{{ctx.Locale.Tr "repo.releases"}}
+						<span class="ui small label">{{.NumReleases}}</span>
+					</a>
 				</div>
-				<div class="flex-item-main">
-					<div class="flex-item-header">
-						<div class="flex-item-title tw-gap-2">
-							<a class="gt-ellipsis muted" href="{{.LatestRelease.Link}}" title="{{.LatestRelease.Title}}">{{.LatestRelease.Title}}</a>
-							<span class="ui basic green label tw-h-100">{{ctx.Locale.Tr "latest"}}</span>
-						</div>
+				<div class="flex-item">
+					<div class="flex-item-icon">
+						{{svg "octicon-tag" 16}}
 					</div>
-					<div class="flex-item-body">
-						<span class="time">{{DateUtils.TimeSince .LatestRelease.CreatedUnix}}</span>
+					<div class="flex-item-main">
+						<div class="flex-item-header">
+							<div class="flex-item-title tw-gap-2">
+								<a class="gt-ellipsis muted" href="{{.LatestRelease.Link}}" title="{{.LatestRelease.Title}}">{{.LatestRelease.Title}}</a>
+								{{template "repo/release/label" (dict "Release" .LatestRelease "IsLatest" true)}}
+							</div>
+						</div>
+						<div class="flex-item-body">
+							<span class="time">{{DateUtils.TimeSince .LatestRelease.CreatedUnix}}</span>
+						</div>
 					</div>
 				</div>
 			</div>
 		</div>
-	</div>
-	{{end}}
+		{{end}}
 
-	{{if and (not .IsEmptyRepo) .LanguageStats}}
-	<div class="flex-item">
-		<div class="flex-item-main">
-			<div class="flex-item-title">
-				{{ctx.Locale.Tr "repo.repo_lang"}}
-			</div>
-
-			<div class="flex-item-body">
-				<div class="language-stats">
-					{{range .LanguageStats}}
-						<div class="bar" style="width: {{.Percentage}}%; background-color: {{.Color}}" data-tooltip-placement="top" data-tooltip-content={{.Language}} data-tooltip-follow-cursor="horizontal"></div>
-					{{end}}
+		{{if and (not .IsEmptyRepo) .LanguageStats}}
+		<div class="flex-item">
+			<div class="flex-item-main">
+				<div class="flex-item-title">
+					{{ctx.Locale.Tr "repo.repo_lang"}}
 				</div>
-				<div class="language-stats-details">
-					{{range .LanguageStats}}
-						<div class="item">
-							<i class="color-icon" style="background-color: {{.Color}}"></i>
-							<span class="tw-font-semibold">
-								{{Iif (eq .Language "other") (ctx.Locale.Tr "repo.language_other") .Language}}
-							</span>
-							{{.Percentage}}%
-						</div>
-					{{end}}
+
+				<div class="flex-item-body">
+					<div class="language-stats">
+						{{range .LanguageStats}}
+							<div class="bar" style="width: {{.Percentage}}%; background-color: {{.Color}}" data-tooltip-placement="top" data-tooltip-content={{.Language}} data-tooltip-follow-cursor="horizontal"></div>
+						{{end}}
+					</div>
+					<div class="language-stats-details">
+						{{range .LanguageStats}}
+							<div class="item">
+								<i class="color-icon" style="background-color: {{.Color}}"></i>
+								<span class="tw-font-semibold">
+									{{Iif (eq .Language "other") (ctx.Locale.Tr "repo.language_other") .Language}}
+								</span>
+								{{.Percentage}}%
+							</div>
+						{{end}}
+					</div>
 				</div>
 			</div>
 		</div>
+		{{end}}
 	</div>
-	{{end}}
 </div>
diff --git a/templates/repo/home_sidebar_top.tmpl b/templates/repo/home_sidebar_top.tmpl
index 4b0ebcd390..607dc62e2e 100644
--- a/templates/repo/home_sidebar_top.tmpl
+++ b/templates/repo/home_sidebar_top.tmpl
@@ -1,68 +1,70 @@
-<form class="ignore-dirty tw-flex tw-flex-1 tw-mt-1" action="{{.RepoLink}}/search" method="get">
-	<div class="ui small action input tw-flex-1">
-		<input name="q" size="10" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
-		{{template "shared/search/button"}}
-	</div>
-</form>
+<div class="repo-home-sidebar-top">
+	<form class="ignore-dirty tw-flex tw-flex-1 tw-mt-1" action="{{.RepoLink}}/search" method="get">
+		<div class="ui small action input tw-flex-1">
+			<input name="q" size="10" placeholder="{{ctx.Locale.Tr "search.code_kind"}}"> {{template "shared/search/button"}}
+		</div>
+	</form>
 
-<div class="flex-list">
-	<div class="flex-item">
-		<div class="flex-item-main">
-			<div class="flex-item-title">
-				{{ctx.Locale.Tr "repo.repo_desc"}}
-			</div>
-			{{if and (not .HideRepoInfo) (not .IsBlame)}}
-				<div class="flex-item-body repo-description tw-break-anywhere tw-gap-2 tw-mt-2">
-					{{- $description := .Repository.DescriptionHTML ctx -}}
-					{{if $description}}{{$description | RenderCodeBlock}}{{else}}{{ctx.Locale.Tr "repo.repo_no_desc"}}{{end}}
-					{{if .Repository.Website}}{{svg "octicon-link"}}<a href="{{.Repository.Website}}">{{.Repository.Website}}</a>{{end}}
-				</div>
-				<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-my-2" id="repo-topics">
-					{{/* !!!! it SHOULD and MUST match the code in issue-home.js */}}
-					{{range .Topics}}<a class="repo-topic ui large label gt-ellipsis" title={{.Name}} href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
-				</div>
-				{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
-					<button id="manage_topic" class="btn interact-fg tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>
-				{{end}}
-			{{end}}
-			{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
-				<div class="ui form tw-hidden flex-item-body tw-gap-2 tw-my-2" id="topic_edit">
-					<div class="ui fluid multiple search selection dropdown tw-flex-wrap tw-flex-1">
-						<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
-						{{range .Topics}}
-							{{/* keep the same layout as Fomantic UI generated labels */}}
-							<a class="ui label transition visible tw-cursor-default tw-inline-block repo-topic" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
+	<div class="flex-list">
+		<div class="flex-item">
+			<div class="flex-item-main">
+				<div class="flex-item-title">{{ctx.Locale.Tr "repo.repo_desc"}}</div>
+				<div class="flex-item-body tw-text-16">
+					<div class="tw-flex tw-flex-col tw-gap-2 tw-mt-2">
+						<div class="repo-description tw-break-anywhere tw-gap-2">
+							{{- $description := .Repository.DescriptionHTML ctx -}}
+							{{if $description}}{{$description | RenderCodeBlock}}{{else}}{{ctx.Locale.Tr "repo.repo_no_desc"}}{{end}}
+						</div>
+
+						{{if .Repository.Website}}
+							<a class="flex-text-block" href="{{.Repository.Website}}">
+								{{svg "octicon-link" 16 "tw-text-text"}} <span class="tw-text-primary">{{.Repository.Website}}</span>
+							</a>
+						{{end}}
+
+						<div id="repo-topics" class="flex-text-block tw-flex-wrap tw-gap-1 tw-my-1 tw-text-text">
+							{{/* !!!! it SHOULD and MUST match the code in repo-home.ts */}}
+							{{range .Topics}}<a class="repo-topic ui large label gt-ellipsis" title={{.Name}} href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
+						</div>
+						{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
+							<button id="manage_topic" class="btn interact-fg tw-mb-2 tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>
+							<div class="ui form tw-hidden tw-my-2" id="topic_edit">
+								<div class="ui fluid multiple search selection dropdown tw-flex-wrap tw-flex-1">
+									<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
+									{{range .Topics}}
+										{{/* keep the same layout as Fomantic UI generated labels */}}
+										<a class="ui label transition visible tw-cursor-default tw-inline-block repo-topic" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
+									{{end}}
+									<div class="text"></div>
+								</div>
+								<div class="tw-my-2">
+									<button class="ui primary button" id="save_topic" data-link="{{.RepoLink}}/topics">{{ctx.Locale.Tr "save"}}</button>
+									<button class="ui basic button" id="cancel_topic_edit">{{ctx.Locale.Tr "cancel"}}</button>
+								</div>
+							</div>
+						{{end}}
+
+						{{if .ReadmeExist}}
+							<a class="flex-text-block muted" href="{{.TreeLink}}/{{.FileName}}">
+								{{svg "octicon-book"}} {{ctx.Locale.Tr "readme"}}
+							</a>
+						{{end}}
+
+						{{if .DetectedRepoLicenses}}
+							<a class="flex-text-block muted" href="{{.RepoLink}}/src/{{.Repository.DefaultBranch}}/{{PathEscapeSegments .LicenseFileName}}" title="{{StringUtils.Join .DetectedRepoLicenses ", "}}">
+								{{svg "octicon-law"}} {{if eq (len .DetectedRepoLicenses) 1}}{{index .DetectedRepoLicenses 0}}{{else}}{{ctx.Locale.Tr "repo.multiple_licenses"}}{{end}}
+							</a>
+						{{end}}
+
+						{{if .CitiationExist}}
+							{{template "repo/cite/cite_modal" .}}
+							<a class="flex-text-block muted" id="cite-repo-button">
+								{{svg "octicon-cross-reference"}} {{ctx.Locale.Tr "repo.cite_this_repo"}}
+							</a>
 						{{end}}
-						<div class="text"></div>
-					</div>
-					<div>
-						<button class="ui primary button" id="save_topic" data-link="{{.RepoLink}}/topics">{{ctx.Locale.Tr "save"}}</button>
-						<button class="ui basic button" id="cancel_topic_edit">{{ctx.Locale.Tr "cancel"}}</button>
 					</div>
 				</div>
-			{{end}}
-			{{if .ReadmeExist}}
-				<div class="flex-item-body tw-mt-2">
-					<a class="flex-text-block muted" href="{{.TreeLink}}/{{.FileName}}">
-						{{svg "octicon-book"}} {{ctx.Locale.Tr "readme"}}
-					</a>
-				</div>
-			{{end}}
-			{{if .DetectedRepoLicenses}}
-				<div class="flex-item-body">
-					<a class="flex-text-block muted" href="{{.RepoLink}}/src/{{.Repository.DefaultBranch}}/{{PathEscapeSegments .LicenseFileName}}" title="{{StringUtils.Join .DetectedRepoLicenses ", "}}">
-						{{svg "octicon-law"}} {{if eq (len .DetectedRepoLicenses) 1}}{{index .DetectedRepoLicenses 0}}{{else}}{{ctx.Locale.Tr "repo.multiple_licenses"}}{{end}}
-					</a>
-				</div>
-			{{end}}
-			{{if .CitiationExist}}
-				<div class="flex-item-body">
-					{{template "repo/cite/cite_modal" .}}
-					<a class="flex-text-block muted" id="cite-repo-button">
-						{{svg "octicon-cross-reference"}} {{ctx.Locale.Tr "repo.cite_this_repo"}}
-					</a>
-				</div>
-			{{end}}
+			</div>
 		</div>
 	</div>
 </div>
diff --git a/templates/repo/issue/branch_selector_field.tmpl b/templates/repo/issue/branch_selector_field.tmpl
index 286ac0cd05..9183b7b46a 100644
--- a/templates/repo/issue/branch_selector_field.tmpl
+++ b/templates/repo/issue/branch_selector_field.tmpl
@@ -1,3 +1,17 @@
+{{/* TODO: RemoveIssueRef: the Issue.Ref will be removed in 1.24 or 1.25 if no end user really needs it or there could be better alternative then.
+PR: https://github.com/go-gitea/gitea/pull/32744
+
+The Issue.Ref was added by Add possibility to record branch or tag information in an issue (#780)
+After 8 years, this "branch selector" does nothing more than saving the branch/tag name into database and displays it.
+
+There are still users using it:
+* @didim99: it is a really useful feature to specify a branch in which issue found.
+
+Still needs to figure out:
+* Could the "recording branch/tag name" be replaced by other approaches?
+	* Write the branch name in the issue title/body then it will still be displayed, eg: `[bug] (fix/ui-broken-bug) there is a bug ....`
+* Is "GitHub-like development sidebar (`#31899`)" good enough (or better) for your usage?
+*/}}
 {{if and (not .Issue.IsPull) (not .PageIsComparePull)}}
 <input id="ref_selector" name="ref" type="hidden" value="{{.Reference}}">
 <div class="ui dropdown select-branch branch-selector-dropdown ellipsis-items-nowrap {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}}"
@@ -29,7 +43,7 @@
 			{{range .Branches}}
 				<div class="item" data-id="refs/heads/{{.}}" data-name="{{.}}" data-id-selector="#ref_selector" title="{{.}}">{{.}}</div>
 			{{else}}
-				<div class="item">{{ctx.Locale.Tr "no_results_found"}}</div>
+				<div class="item disabled">{{ctx.Locale.Tr "no_results_found"}}</div>
 			{{end}}
 		</div>
 		<div id="tag-list" class="scrolling menu reference-list-menu tw-hidden">
@@ -39,7 +53,7 @@
 			{{range .Tags}}
 				<div class="item" data-id="refs/tags/{{.}}" data-name="tags/{{.}}" data-id-selector="#ref_selector">{{.}}</div>
 			{{else}}
-				<div class="item">{{ctx.Locale.Tr "no_results_found"}}</div>
+				<div class="item disabled">{{ctx.Locale.Tr "no_results_found"}}</div>
 			{{end}}
 		</div>
 	</div>
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index ceaaebc4d5..dd4c7617ce 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -47,7 +47,8 @@
 	</div>
 
 	<div class="issue-content-right ui segment">
-		{{template "repo/issue/branch_selector_field" $}}
+		{{template "repo/issue/branch_selector_field" $}}{{/* TODO: RemoveIssueRef: template "repo/issue/branch_selector_field" $*/}}
+
 		{{if .PageIsComparePull}}
 			{{template "repo/issue/sidebar/reviewer_list" $.IssuePageMetaData}}
 			<div class="divider"></div>
diff --git a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl
index 1acf56d7b2..f107dc5ef5 100644
--- a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl
+++ b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl
@@ -44,7 +44,7 @@
 				<form method="post" class="ui form form-fetch-action" action="{{.Issue.Link}}/time_estimate">
 					<div class="content">
 						{{$.CsrfTokenHtml}}
-						<input name="time_estimate" placeholder="{{ctx.Locale.Tr "repo.issues.time_estimate_placeholder"}}" value="{{TimeEstimateString .Issue.TimeEstimate}}">
+						<input name="time_estimate" placeholder="1h 2m" value="{{TimeEstimateString .Issue.TimeEstimate}}">
 						<div class="actions">
 							<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
 							<button class="ui primary button">{{ctx.Locale.Tr "repo.issues.save"}}</button>
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 02f5d3e2df..987a882be7 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -1,5 +1,5 @@
 <div class="issue-content-right ui segment">
-	{{template "repo/issue/branch_selector_field" $}}
+	{{template "repo/issue/branch_selector_field" $}}{{/* TODO: RemoveIssueRef: template "repo/issue/branch_selector_field" $*/}}
 
 	{{if .Issue.IsPull}}
 		{{template "repo/issue/sidebar/reviewer_list" $.IssuePageMetaData}}
diff --git a/templates/repo/release/label.tmpl b/templates/repo/release/label.tmpl
new file mode 100644
index 0000000000..eacb3e36f4
--- /dev/null
+++ b/templates/repo/release/label.tmpl
@@ -0,0 +1,14 @@
+{{/*
+Template Attributes:
+* Release: the release
+* IsLatest: boolean indicating whether this is the latest release, optional
+*/}}
+{{if .IsLatest}}
+	<span class="ui green label">{{ctx.Locale.Tr "repo.release.latest"}}</span>
+{{else if .Release.IsDraft}}
+	<span class="ui yellow label">{{ctx.Locale.Tr "repo.release.draft"}}</span>
+{{else if .Release.IsPrerelease}}
+	<span class="ui orange label">{{ctx.Locale.Tr "repo.release.prerelease"}}</span>
+{{else if (not .Release.IsTag)}}
+	<span class="ui green label">{{ctx.Locale.Tr "repo.release.stable"}}</span>
+{{end}}
diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl
index efaac4432a..99934d2118 100644
--- a/templates/repo/release/list.tmpl
+++ b/templates/repo/release/list.tmpl
@@ -33,13 +33,7 @@
 							<h4 class="release-list-title tw-break-anywhere">
 								{{if $.PageIsSingleTag}}{{$release.Title}}{{else}}<a class="muted" href="{{$.RepoLink}}/releases/tag/{{$release.TagName | PathEscapeSegments}}">{{$release.Title}}</a>{{end}}
 								{{template "repo/commit_statuses" dict "Status" $info.CommitStatus "Statuses" $info.CommitStatuses "AdditionalClasses" "tw-flex"}}
-								{{if $release.IsDraft}}
-									<span class="ui yellow label">{{ctx.Locale.Tr "repo.release.draft"}}</span>
-								{{else if $release.IsPrerelease}}
-									<span class="ui orange label">{{ctx.Locale.Tr "repo.release.prerelease"}}</span>
-								{{else if (not $release.IsTag)}}
-									<span class="ui green label">{{ctx.Locale.Tr "repo.release.stable"}}</span>
-								{{end}}
+								{{template "repo/release/label" (dict "Release" $release)}}
 							</h4>
 							<div>
 								{{if and $.CanCreateRelease (not $.PageIsSingleTag)}}
diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl
index ea61c3736a..2d555e4c2e 100644
--- a/templates/repo/view_list.tmpl
+++ b/templates/repo/view_list.tmpl
@@ -1,13 +1,13 @@
 {{/* use grid layout, still use the old ID because there are many other CSS styles depending on this ID */}}
 <div id="repo-files-table" {{if .HasFilesWithoutLatestCommit}}hx-indicator="#repo-files-table .repo-file-cell.message" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
-	<div class="repo-file-line">
+	<div class="repo-file-line repo-file-last-commit">
 		<div class="latest-commit">{{template "repo/latest_commit" .}}</div>
 		<div>{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}</div>
 	</div>
 	{{if .HasParentPath}}
-	<div class="repo-file-line">
-		{{svg "octicon-reply"}} <a class="muted" href="{{.BranchLink}}{{if .ParentPath}}{{PathEscapeSegments .ParentPath}}{{end}}">..</a>
-	</div>
+	<a class="repo-file-line parent-link silenced" href="{{.BranchLink}}{{if .ParentPath}}{{PathEscapeSegments .ParentPath}}{{end}}">
+		{{svg "octicon-file-directory-fill"}} ..
+	</a>
 	{{end}}
 	{{range $item := .Files}}
 		<div class="repo-file-item">
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl
index fe5184e7d2..a2b802f2a2 100644
--- a/templates/shared/issuelist.tmpl
+++ b/templates/shared/issuelist.tmpl
@@ -99,7 +99,7 @@
 							<span class="gt-ellipsis">{{.Project.Title}}</span>
 						</a>
 					{{end}}
-					{{if .Ref}}
+					{{if .Ref}}{{/* TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl" */}}
 						<a class="ref flex-text-inline tw-max-w-[300px]" {{if $.RepoLink}}href="{{index $.IssueRefURLs .ID}}"{{else}}href="{{.Repo.Link}}{{index $.IssueRefURLs .ID}}"{{end}}>
 							{{svg "octicon-git-branch" 14}}
 							<span class="gt-ellipsis">{{index $.IssueRefEndNames .ID}}</span>
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index 3124048d36..9daa051e06 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -48,7 +48,7 @@
 			</div>
 		</form>
 		{{end}}{{/*if .EnablePasswordSignInForm*/}}
-		{{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn}}
+		{{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}}
 		{{if and $showOAuth2Methods .EnablePasswordSignInForm}}
 			<div class="divider divider-text">{{ctx.Locale.Tr "sign_in_or"}}</div>
 		{{end}}
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl
index 41d0cd49b5..ea8d0bafe4 100644
--- a/templates/user/auth/signup_inner.tmpl
+++ b/templates/user/auth/signup_inner.tmpl
@@ -47,7 +47,7 @@
 					</button>
 				</div>
 			{{end}}
-			{{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn}}
+			{{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}}
 			{{if $showOAuth2Methods}}
 				<div class="divider divider-text">{{ctx.Locale.Tr "sign_in_or"}}</div>
 				{{template "user/auth/oauth_container" .}}
diff --git a/tests/integration/api_actions_artifact_test.go b/tests/integration/api_actions_artifact_test.go
index 29e9930538..6393fc53cc 100644
--- a/tests/integration/api_actions_artifact_test.go
+++ b/tests/integration/api_actions_artifact_test.go
@@ -133,7 +133,7 @@ func TestActionsArtifactDownload(t *testing.T) {
 		}
 	}
 	assert.NotNil(t, artifactIdx)
-	assert.Equal(t, listResp.Value[artifactIdx].Name, "artifact-download")
+	assert.Equal(t, "artifact-download", listResp.Value[artifactIdx].Name)
 	assert.Contains(t, listResp.Value[artifactIdx].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
 
 	idx := strings.Index(listResp.Value[artifactIdx].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
@@ -374,7 +374,7 @@ func TestActionsArtifactOverwrite(t *testing.T) {
 				break
 			}
 		}
-		assert.Equal(t, uploadedItem.Name, "artifact-download")
+		assert.Equal(t, "artifact-download", uploadedItem.Name)
 
 		idx := strings.Index(uploadedItem.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
 		url := uploadedItem.FileContainerResourceURL[idx+1:] + "?itemPath=artifact-download"
diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go
index 24a041de17..8a0bd2e4ff 100644
--- a/tests/integration/api_branch_test.go
+++ b/tests/integration/api_branch_test.go
@@ -244,7 +244,7 @@ func TestAPIBranchProtection(t *testing.T) {
 		StatusCheckContexts: []string{"test1"},
 	}, http.StatusOK)
 	bp := testAPIGetBranchProtection(t, "master", http.StatusOK)
-	assert.Equal(t, true, bp.EnableStatusCheck)
+	assert.True(t, bp.EnableStatusCheck)
 	assert.Equal(t, []string{"test1"}, bp.StatusCheckContexts)
 
 	// disable status checks, clear the list of required checks
@@ -253,7 +253,7 @@ func TestAPIBranchProtection(t *testing.T) {
 		StatusCheckContexts: []string{},
 	}, http.StatusOK)
 	bp = testAPIGetBranchProtection(t, "master", http.StatusOK)
-	assert.Equal(t, false, bp.EnableStatusCheck)
+	assert.False(t, bp.EnableStatusCheck)
 	assert.Equal(t, []string{}, bp.StatusCheckContexts)
 
 	testAPIDeleteBranchProtection(t, "master", http.StatusNoContent)
diff --git a/tests/integration/api_issue_config_test.go b/tests/integration/api_issue_config_test.go
index 745d0cb2a2..ad39965443 100644
--- a/tests/integration/api_issue_config_test.go
+++ b/tests/integration/api_issue_config_test.go
@@ -47,7 +47,7 @@ func TestAPIRepoGetIssueConfig(t *testing.T) {
 		issueConfig := getIssueConfig(t, owner.Name, repo.Name)
 
 		assert.True(t, issueConfig.BlankIssuesEnabled)
-		assert.Len(t, issueConfig.ContactLinks, 0)
+		assert.Empty(t, issueConfig.ContactLinks)
 	})
 
 	t.Run("DisableBlankIssues", func(t *testing.T) {
@@ -59,7 +59,7 @@ func TestAPIRepoGetIssueConfig(t *testing.T) {
 		issueConfig := getIssueConfig(t, owner.Name, repo.Name)
 
 		assert.False(t, issueConfig.BlankIssuesEnabled)
-		assert.Len(t, issueConfig.ContactLinks, 0)
+		assert.Empty(t, issueConfig.ContactLinks)
 	})
 
 	t.Run("ContactLinks", func(t *testing.T) {
@@ -135,7 +135,7 @@ func TestAPIRepoIssueConfigPaths(t *testing.T) {
 				issueConfig := getIssueConfig(t, owner.Name, repo.Name)
 
 				assert.False(t, issueConfig.BlankIssuesEnabled)
-				assert.Len(t, issueConfig.ContactLinks, 0)
+				assert.Empty(t, issueConfig.ContactLinks)
 
 				_, err = deleteFileInBranch(owner, repo, fullPath, repo.DefaultBranch)
 				assert.NoError(t, err)
diff --git a/tests/integration/api_issue_pin_test.go b/tests/integration/api_issue_pin_test.go
index 1cff937254..c1bfa5aa0e 100644
--- a/tests/integration/api_issue_pin_test.go
+++ b/tests/integration/api_issue_pin_test.go
@@ -153,7 +153,7 @@ func TestAPIListPinnedIssues(t *testing.T) {
 	var issueList []api.Issue
 	DecodeJSON(t, resp, &issueList)
 
-	assert.Equal(t, 1, len(issueList))
+	assert.Len(t, issueList, 1)
 	assert.Equal(t, issue.ID, issueList[0].ID)
 }
 
@@ -169,7 +169,7 @@ func TestAPIListPinnedPullrequests(t *testing.T) {
 	var prList []api.PullRequest
 	DecodeJSON(t, resp, &prList)
 
-	assert.Equal(t, 0, len(prList))
+	assert.Empty(t, prList)
 }
 
 func TestAPINewPinAllowed(t *testing.T) {
diff --git a/tests/integration/api_issue_stopwatch_test.go b/tests/integration/api_issue_stopwatch_test.go
index 2306678217..4765787e6f 100644
--- a/tests/integration/api_issue_stopwatch_test.go
+++ b/tests/integration/api_issue_stopwatch_test.go
@@ -40,7 +40,7 @@ func TestAPIListStopWatches(t *testing.T) {
 		assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle)
 		assert.EqualValues(t, repo.Name, apiWatches[0].RepoName)
 		assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName)
-		assert.Greater(t, apiWatches[0].Seconds, int64(0))
+		assert.Positive(t, apiWatches[0].Seconds)
 	}
 }
 
diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go
index 9f75478ebf..d8394a33d9 100644
--- a/tests/integration/api_issue_test.go
+++ b/tests/integration/api_issue_test.go
@@ -252,7 +252,7 @@ func TestAPIEditIssue(t *testing.T) {
 	assert.Equal(t, api.StateClosed, apiIssue.State)
 	assert.Equal(t, milestone, apiIssue.Milestone.ID)
 	assert.Equal(t, body, apiIssue.Body)
-	assert.True(t, apiIssue.Deadline == nil)
+	assert.Nil(t, apiIssue.Deadline)
 	assert.Equal(t, title, apiIssue.Title)
 
 	// in database
diff --git a/tests/integration/api_keys_test.go b/tests/integration/api_keys_test.go
index 89ad1ec0df..2276b955cf 100644
--- a/tests/integration/api_keys_test.go
+++ b/tests/integration/api_keys_test.go
@@ -168,7 +168,7 @@ func TestCreateUserKey(t *testing.T) {
 	resp = MakeRequest(t, req, http.StatusOK)
 
 	DecodeJSON(t, resp, &fingerprintPublicKeys)
-	assert.Len(t, fingerprintPublicKeys, 0)
+	assert.Empty(t, fingerprintPublicKeys)
 
 	// Fail searching for wrong users key
 	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/keys?fingerprint=%s", "user2", newPublicKey.Fingerprint)).
@@ -176,7 +176,7 @@ func TestCreateUserKey(t *testing.T) {
 	resp = MakeRequest(t, req, http.StatusOK)
 
 	DecodeJSON(t, resp, &fingerprintPublicKeys)
-	assert.Len(t, fingerprintPublicKeys, 0)
+	assert.Empty(t, fingerprintPublicKeys)
 
 	// Now login as user 2
 	session2 := loginUser(t, "user2")
@@ -208,5 +208,5 @@ func TestCreateUserKey(t *testing.T) {
 	resp = MakeRequest(t, req, http.StatusOK)
 
 	DecodeJSON(t, resp, &fingerprintPublicKeys)
-	assert.Len(t, fingerprintPublicKeys, 0)
+	assert.Empty(t, fingerprintPublicKeys)
 }
diff --git a/tests/integration/api_notification_test.go b/tests/integration/api_notification_test.go
index abb9852eef..dc4ba83ecc 100644
--- a/tests/integration/api_notification_test.go
+++ b/tests/integration/api_notification_test.go
@@ -120,7 +120,7 @@ func TestAPINotification(t *testing.T) {
 		AddTokenAuth(token)
 	resp = MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &newStruct)
-	assert.True(t, newStruct.New > 0)
+	assert.Positive(t, newStruct.New)
 
 	// -- mark notifications as read --
 	req = NewRequest(t, "GET", "/api/v1/notifications?status-types=unread").
@@ -154,7 +154,7 @@ func TestAPINotification(t *testing.T) {
 		AddTokenAuth(token)
 	resp = MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &newStruct)
-	assert.True(t, newStruct.New == 0)
+	assert.Zero(t, newStruct.New)
 }
 
 func TestAPINotificationPUT(t *testing.T) {
diff --git a/tests/integration/api_oauth2_apps_test.go b/tests/integration/api_oauth2_apps_test.go
index 0ea3dc72ff..7a17b4ca88 100644
--- a/tests/integration/api_oauth2_apps_test.go
+++ b/tests/integration/api_oauth2_apps_test.go
@@ -74,9 +74,9 @@ func testAPIListOAuth2Applications(t *testing.T) {
 	DecodeJSON(t, resp, &appList)
 	expectedApp := appList[0]
 
-	assert.EqualValues(t, existApp.Name, expectedApp.Name)
-	assert.EqualValues(t, existApp.ClientID, expectedApp.ClientID)
-	assert.Equal(t, existApp.ConfidentialClient, expectedApp.ConfidentialClient)
+	assert.EqualValues(t, expectedApp.Name, existApp.Name)
+	assert.EqualValues(t, expectedApp.ClientID, existApp.ClientID)
+	assert.Equal(t, expectedApp.ConfidentialClient, existApp.ConfidentialClient)
 	assert.Len(t, expectedApp.ClientID, 36)
 	assert.Empty(t, expectedApp.ClientSecret)
 	assert.EqualValues(t, existApp.RedirectURIs[0], expectedApp.RedirectURIs[0])
@@ -128,13 +128,13 @@ func testAPIGetOAuth2Application(t *testing.T) {
 	DecodeJSON(t, resp, &app)
 	expectedApp := app
 
-	assert.EqualValues(t, existApp.Name, expectedApp.Name)
-	assert.EqualValues(t, existApp.ClientID, expectedApp.ClientID)
-	assert.Equal(t, existApp.ConfidentialClient, expectedApp.ConfidentialClient)
+	assert.EqualValues(t, expectedApp.Name, existApp.Name)
+	assert.EqualValues(t, expectedApp.ClientID, existApp.ClientID)
+	assert.Equal(t, expectedApp.ConfidentialClient, existApp.ConfidentialClient)
 	assert.Len(t, expectedApp.ClientID, 36)
 	assert.Empty(t, expectedApp.ClientSecret)
 	assert.Len(t, expectedApp.RedirectURIs, 1)
-	assert.EqualValues(t, existApp.RedirectURIs[0], expectedApp.RedirectURIs[0])
+	assert.EqualValues(t, expectedApp.RedirectURIs[0], existApp.RedirectURIs[0])
 	unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: expectedApp.ID, Name: expectedApp.Name})
 }
 
diff --git a/tests/integration/api_packages_npm_test.go b/tests/integration/api_packages_npm_test.go
index 9c888972ff..b9660aeeb9 100644
--- a/tests/integration/api_packages_npm_test.go
+++ b/tests/integration/api_packages_npm_test.go
@@ -325,7 +325,7 @@ func TestPackageNpm(t *testing.T) {
 
 			pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm)
 			assert.NoError(t, err)
-			assert.Len(t, pvs, 0)
+			assert.Empty(t, pvs)
 		})
 	})
 }
diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go
index d26b285a1a..969e110895 100644
--- a/tests/integration/api_pull_test.go
+++ b/tests/integration/api_pull_test.go
@@ -48,7 +48,7 @@ func TestAPIViewPulls(t *testing.T) {
 	pull := pulls[0]
 	assert.EqualValues(t, 1, pull.Poster.ID)
 	assert.Len(t, pull.RequestedReviewers, 2)
-	assert.Len(t, pull.RequestedReviewersTeams, 0)
+	assert.Empty(t, pull.RequestedReviewersTeams)
 	assert.EqualValues(t, 5, pull.RequestedReviewers[0].ID)
 	assert.EqualValues(t, 6, pull.RequestedReviewers[1].ID)
 	assert.EqualValues(t, 1, pull.ChangedFiles)
@@ -83,7 +83,7 @@ func TestAPIViewPulls(t *testing.T) {
 	pull = pulls[1]
 	assert.EqualValues(t, 1, pull.Poster.ID)
 	assert.Len(t, pull.RequestedReviewers, 4)
-	assert.Len(t, pull.RequestedReviewersTeams, 0)
+	assert.Empty(t, pull.RequestedReviewersTeams)
 	assert.EqualValues(t, 3, pull.RequestedReviewers[0].ID)
 	assert.EqualValues(t, 4, pull.RequestedReviewers[1].ID)
 	assert.EqualValues(t, 2, pull.RequestedReviewers[2].ID)
@@ -120,7 +120,7 @@ func TestAPIViewPulls(t *testing.T) {
 	pull = pulls[2]
 	assert.EqualValues(t, 1, pull.Poster.ID)
 	assert.Len(t, pull.RequestedReviewers, 1)
-	assert.Len(t, pull.RequestedReviewersTeams, 0)
+	assert.Empty(t, pull.RequestedReviewersTeams)
 	assert.EqualValues(t, 1, pull.RequestedReviewers[0].ID)
 	assert.EqualValues(t, 0, pull.ChangedFiles)
 
diff --git a/tests/integration/api_repo_git_commits_test.go b/tests/integration/api_repo_git_commits_test.go
index 3655206207..c4c626eb49 100644
--- a/tests/integration/api_repo_git_commits_test.go
+++ b/tests/integration/api_repo_git_commits_test.go
@@ -77,7 +77,7 @@ func TestAPIReposGitCommitList(t *testing.T) {
 	assert.EqualValues(t, "c8e31bc7688741a5287fcde4fbb8fc129ca07027", apiData[1].CommitMeta.SHA)
 	compareCommitFiles(t, []string{"test.csv"}, apiData[1].Files)
 
-	assert.EqualValues(t, resp.Header().Get("X-Total"), "2")
+	assert.EqualValues(t, "2", resp.Header().Get("X-Total"))
 }
 
 func TestAPIReposGitCommitListNotMaster(t *testing.T) {
@@ -103,7 +103,7 @@ func TestAPIReposGitCommitListNotMaster(t *testing.T) {
 	assert.EqualValues(t, "5099b81332712fe655e34e8dd63574f503f61811", apiData[2].CommitMeta.SHA)
 	compareCommitFiles(t, []string{"readme.md"}, apiData[2].Files)
 
-	assert.EqualValues(t, resp.Header().Get("X-Total"), "3")
+	assert.EqualValues(t, "3", resp.Header().Get("X-Total"))
 }
 
 func TestAPIReposGitCommitListPage2Empty(t *testing.T) {
@@ -121,7 +121,7 @@ func TestAPIReposGitCommitListPage2Empty(t *testing.T) {
 	var apiData []api.Commit
 	DecodeJSON(t, resp, &apiData)
 
-	assert.Len(t, apiData, 0)
+	assert.Empty(t, apiData)
 }
 
 func TestAPIReposGitCommitListDifferentBranch(t *testing.T) {
@@ -208,7 +208,7 @@ func TestGetFileHistory(t *testing.T) {
 	assert.Equal(t, "f27c2b2b03dcab38beaf89b0ab4ff61f6de63441", apiData[0].CommitMeta.SHA)
 	compareCommitFiles(t, []string{"readme.md"}, apiData[0].Files)
 
-	assert.EqualValues(t, resp.Header().Get("X-Total"), "1")
+	assert.EqualValues(t, "1", resp.Header().Get("X-Total"))
 }
 
 func TestGetFileHistoryNotOnMaster(t *testing.T) {
@@ -229,5 +229,5 @@ func TestGetFileHistoryNotOnMaster(t *testing.T) {
 	assert.Equal(t, "c8e31bc7688741a5287fcde4fbb8fc129ca07027", apiData[0].CommitMeta.SHA)
 	compareCommitFiles(t, []string{"test.csv"}, apiData[0].Files)
 
-	assert.EqualValues(t, resp.Header().Get("X-Total"), "1")
+	assert.EqualValues(t, "1", resp.Header().Get("X-Total"))
 }
diff --git a/tests/integration/api_repo_lfs_locks_test.go b/tests/integration/api_repo_lfs_locks_test.go
index 427e0b9fb1..4ba01e6d9b 100644
--- a/tests/integration/api_repo_lfs_locks_test.go
+++ b/tests/integration/api_repo_lfs_locks_test.go
@@ -176,6 +176,6 @@ func TestAPILFSLocksLogged(t *testing.T) {
 		resp := session.MakeRequest(t, req, http.StatusOK)
 		var lfsLocks api.LFSLockList
 		DecodeJSON(t, resp, &lfsLocks)
-		assert.Len(t, lfsLocks.Locks, 0)
+		assert.Empty(t, lfsLocks.Locks)
 	}
 }
diff --git a/tests/integration/api_repo_teams_test.go b/tests/integration/api_repo_teams_test.go
index 558bac8150..07d065b02b 100644
--- a/tests/integration/api_repo_teams_test.go
+++ b/tests/integration/api_repo_teams_test.go
@@ -39,7 +39,7 @@ func TestAPIRepoTeams(t *testing.T) {
 	if assert.Len(t, teams, 2) {
 		assert.EqualValues(t, "Owners", teams[0].Name)
 		assert.True(t, teams[0].CanCreateOrgRepo)
-		assert.True(t, util.SliceSortedEqual(unit.AllUnitKeyNames(), teams[0].Units), fmt.Sprintf("%v == %v", unit.AllUnitKeyNames(), teams[0].Units))
+		assert.True(t, util.SliceSortedEqual(unit.AllUnitKeyNames(), teams[0].Units), "%v == %v", unit.AllUnitKeyNames(), teams[0].Units)
 		assert.EqualValues(t, "owner", teams[0].Permission)
 
 		assert.EqualValues(t, "test_team", teams[1].Name)
diff --git a/tests/integration/api_user_orgs_test.go b/tests/integration/api_user_orgs_test.go
index c656ded5ae..9b8726c6c2 100644
--- a/tests/integration/api_user_orgs_test.go
+++ b/tests/integration/api_user_orgs_test.go
@@ -76,7 +76,7 @@ func TestUserOrgs(t *testing.T) {
 
 	// unrelated user should not get private org membership of privateMemberUsername
 	orgs = getUserOrgs(t, unrelatedUsername, privateMemberUsername)
-	assert.Len(t, orgs, 0)
+	assert.Empty(t, orgs)
 
 	// not authenticated call should not be allowed
 	testUserOrgsUnauthenticated(t, privateMemberUsername)
diff --git a/tests/integration/api_user_search_test.go b/tests/integration/api_user_search_test.go
index e9805a5139..5604a14259 100644
--- a/tests/integration/api_user_search_test.go
+++ b/tests/integration/api_user_search_test.go
@@ -49,7 +49,7 @@ func TestAPIUserSearchLoggedIn(t *testing.T) {
 	for _, user := range results.Data {
 		assert.Contains(t, user.UserName, query)
 		assert.NotEmpty(t, user.Email)
-		assert.True(t, user.Visibility == "public")
+		assert.Equal(t, "public", user.Visibility)
 	}
 }
 
@@ -83,7 +83,7 @@ func TestAPIUserSearchSystemUsers(t *testing.T) {
 			var results SearchResults
 			DecodeJSON(t, resp, &results)
 			assert.NotEmpty(t, results.Data)
-			if assert.EqualValues(t, 1, len(results.Data)) {
+			if assert.Len(t, results.Data, 1) {
 				user := results.Data[0]
 				assert.EqualValues(t, user.UserName, systemUser.Name)
 				assert.EqualValues(t, user.ID, systemUser.ID)
@@ -137,7 +137,7 @@ func TestAPIUserSearchByEmail(t *testing.T) {
 
 	var results SearchResults
 	DecodeJSON(t, resp, &results)
-	assert.Equal(t, 1, len(results.Data))
+	assert.Len(t, results.Data, 1)
 	assert.Equal(t, query, results.Data[0].Email)
 
 	// no login user can not search user with private email
@@ -155,6 +155,6 @@ func TestAPIUserSearchByEmail(t *testing.T) {
 	resp = MakeRequest(t, req, http.StatusOK)
 
 	DecodeJSON(t, resp, &results)
-	assert.Equal(t, 1, len(results.Data))
+	assert.Len(t, results.Data, 1)
 	assert.Equal(t, query, results.Data[0].Email)
 }
diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go
index 00ef72c1c3..5d37244331 100644
--- a/tests/integration/auth_ldap_test.go
+++ b/tests/integration/auth_ldap_test.go
@@ -15,13 +15,17 @@ import (
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/optional"
+	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/translation"
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/auth"
 	"code.gitea.io/gitea/services/auth/source/ldap"
 	org_service "code.gitea.io/gitea/services/org"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 type ldapUser struct {
@@ -35,87 +39,97 @@ type ldapUser struct {
 	SSHKeys      []string
 }
 
-var gitLDAPUsers = []ldapUser{
-	{
-		UserName:    "professor",
-		Password:    "professor",
-		FullName:    "Hubert Farnsworth",
-		Email:       "professor@planetexpress.com",
-		OtherEmails: []string{"hubert@planetexpress.com"},
-		IsAdmin:     true,
-	},
-	{
-		UserName: "hermes",
-		Password: "hermes",
-		FullName: "Conrad Hermes",
-		Email:    "hermes@planetexpress.com",
-		SSHKeys: []string{
-			"SHA256:qLY06smKfHoW/92yXySpnxFR10QFrLdRjf/GNPvwcW8",
-			"SHA256:QlVTuM5OssDatqidn2ffY+Lc4YA5Fs78U+0KOHI51jQ",
-			"SHA256:DXdeUKYOJCSSmClZuwrb60hUq7367j4fA+udNC3FdRI",
+type ldapTestEnv struct {
+	gitLDAPUsers   []ldapUser
+	otherLDAPUsers []ldapUser
+	serverHost     string
+	serverPort     string
+}
+
+func prepareLdapTestEnv(t *testing.T) *ldapTestEnv {
+	if os.Getenv("TEST_LDAP") != "1" {
+		t.Skip()
+		return nil
+	}
+
+	gitLDAPUsers := []ldapUser{
+		{
+			UserName:    "professor",
+			Password:    "professor",
+			FullName:    "Hubert Farnsworth",
+			Email:       "professor@planetexpress.com",
+			OtherEmails: []string{"hubert@planetexpress.com"},
+			IsAdmin:     true,
+		},
+		{
+			UserName: "hermes",
+			Password: "hermes",
+			FullName: "Conrad Hermes",
+			Email:    "hermes@planetexpress.com",
+			SSHKeys: []string{
+				"SHA256:qLY06smKfHoW/92yXySpnxFR10QFrLdRjf/GNPvwcW8",
+				"SHA256:QlVTuM5OssDatqidn2ffY+Lc4YA5Fs78U+0KOHI51jQ",
+				"SHA256:DXdeUKYOJCSSmClZuwrb60hUq7367j4fA+udNC3FdRI",
+			},
+			IsAdmin: true,
+		},
+		{
+			UserName: "fry",
+			Password: "fry",
+			FullName: "Philip Fry",
+			Email:    "fry@planetexpress.com",
+		},
+		{
+			UserName:     "leela",
+			Password:     "leela",
+			FullName:     "Leela Turanga",
+			Email:        "leela@planetexpress.com",
+			IsRestricted: true,
+		},
+		{
+			UserName: "bender",
+			Password: "bender",
+			FullName: "Bender Rodríguez",
+			Email:    "bender@planetexpress.com",
 		},
-		IsAdmin: true,
-	},
-	{
-		UserName: "fry",
-		Password: "fry",
-		FullName: "Philip Fry",
-		Email:    "fry@planetexpress.com",
-	},
-	{
-		UserName:     "leela",
-		Password:     "leela",
-		FullName:     "Leela Turanga",
-		Email:        "leela@planetexpress.com",
-		IsRestricted: true,
-	},
-	{
-		UserName: "bender",
-		Password: "bender",
-		FullName: "Bender Rodríguez",
-		Email:    "bender@planetexpress.com",
-	},
-}
-
-var otherLDAPUsers = []ldapUser{
-	{
-		UserName: "zoidberg",
-		Password: "zoidberg",
-		FullName: "John Zoidberg",
-		Email:    "zoidberg@planetexpress.com",
-	},
-	{
-		UserName: "amy",
-		Password: "amy",
-		FullName: "Amy Kroker",
-		Email:    "amy@planetexpress.com",
-	},
-}
-
-func skipLDAPTests() bool {
-	return os.Getenv("TEST_LDAP") != "1"
-}
-
-func getLDAPServerHost() string {
-	host := os.Getenv("TEST_LDAP_HOST")
-	if len(host) == 0 {
-		host = "ldap"
 	}
-	return host
-}
 
-func getLDAPServerPort() string {
-	port := os.Getenv("TEST_LDAP_PORT")
-	if len(port) == 0 {
-		port = "389"
+	otherLDAPUsers := []ldapUser{
+		{
+			UserName: "zoidberg",
+			Password: "zoidberg",
+			FullName: "John Zoidberg",
+			Email:    "zoidberg@planetexpress.com",
+		},
+		{
+			UserName: "amy",
+			Password: "amy",
+			FullName: "Amy Kroker",
+			Email:    "amy@planetexpress.com",
+		},
+	}
+
+	return &ldapTestEnv{
+		gitLDAPUsers:   gitLDAPUsers,
+		otherLDAPUsers: otherLDAPUsers,
+		serverHost:     util.IfZero(os.Getenv("TEST_LDAP_HOST"), "ldap"),
+		serverPort:     util.IfZero(os.Getenv("TEST_LDAP_PORT"), "389"),
 	}
-	return port
 }
 
-func buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval string) map[string]string {
+type ldapAuthOptions struct {
+	attributeUID          optional.Option[string] // defaults to "uid"
+	attributeSSHPublicKey string
+	groupFilter           string
+	groupTeamMap          string
+	groupTeamMapRemoval   string
+}
+
+func (te *ldapTestEnv) buildAuthSourcePayload(csrf string, opts ...ldapAuthOptions) map[string]string {
+	opt := util.OptionalArg(opts)
 	// Modify user filter to test group filter explicitly
 	userFilter := "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))"
-	if groupFilter != "" {
+	if opt.groupFilter != "" {
 		userFilter = "(&(objectClass=inetOrgPerson)(uid=%s))"
 	}
 
@@ -123,53 +137,47 @@ func buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap
 		"_csrf":                    csrf,
 		"type":                     "2",
 		"name":                     "ldap",
-		"host":                     getLDAPServerHost(),
-		"port":                     getLDAPServerPort(),
+		"host":                     te.serverHost,
+		"port":                     te.serverPort,
 		"bind_dn":                  "uid=gitea,ou=service,dc=planetexpress,dc=com",
 		"bind_password":            "password",
 		"user_base":                "ou=people,dc=planetexpress,dc=com",
 		"filter":                   userFilter,
 		"admin_filter":             "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)",
 		"restricted_filter":        "(uid=leela)",
-		"attribute_username":       "uid",
+		"attribute_username":       util.Iif(opt.attributeUID.Has(), opt.attributeUID.Value(), "uid"),
 		"attribute_name":           "givenName",
 		"attribute_surname":        "sn",
 		"attribute_mail":           "mail",
-		"attribute_ssh_public_key": sshKeyAttribute,
+		"attribute_ssh_public_key": opt.attributeSSHPublicKey,
 		"is_sync_enabled":          "on",
 		"is_active":                "on",
 		"groups_enabled":           "on",
 		"group_dn":                 "ou=people,dc=planetexpress,dc=com",
 		"group_member_uid":         "member",
-		"group_filter":             groupFilter,
-		"group_team_map":           groupTeamMap,
-		"group_team_map_removal":   groupTeamMapRemoval,
+		"group_filter":             opt.groupFilter,
+		"group_team_map":           opt.groupTeamMap,
+		"group_team_map_removal":   opt.groupTeamMapRemoval,
 		"user_uid":                 "DN",
 	}
 }
 
-func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupMapParams ...string) {
-	groupTeamMapRemoval := "off"
-	groupTeamMap := ""
-	if len(groupMapParams) == 2 {
-		groupTeamMapRemoval = groupMapParams[0]
-		groupTeamMap = groupMapParams[1]
-	}
+func (te *ldapTestEnv) addAuthSource(t *testing.T, opts ...ldapAuthOptions) {
 	session := loginUser(t, "user1")
 	csrf := GetUserCSRFToken(t, session)
-	req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval))
+	req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", te.buildAuthSourcePayload(csrf, opts...))
 	session.MakeRequest(t, req, http.StatusSeeOther)
 }
 
 func TestLDAPUserSignin(t *testing.T) {
-	if skipLDAPTests() {
-		t.Skip()
+	te := prepareLdapTestEnv(t)
+	if te == nil {
 		return
 	}
 	defer tests.PrepareTestEnv(t)()
-	addAuthSourceLDAP(t, "", "")
+	te.addAuthSource(t)
 
-	u := gitLDAPUsers[0]
+	u := te.gitLDAPUsers[0]
 
 	session := loginUserWithPassword(t, u.UserName, u.Password)
 	req := NewRequest(t, "GET", "/user/settings")
@@ -183,8 +191,13 @@ func TestLDAPUserSignin(t *testing.T) {
 }
 
 func TestLDAPAuthChange(t *testing.T) {
+	te := prepareLdapTestEnv(t)
+	if te == nil {
+		return
+	}
+
 	defer tests.PrepareTestEnv(t)()
-	addAuthSourceLDAP(t, "", "")
+	te.addAuthSource(t)
 
 	session := loginUser(t, "user1")
 	req := NewRequest(t, "GET", "/-/admin/auths")
@@ -201,34 +214,35 @@ func TestLDAPAuthChange(t *testing.T) {
 	doc = NewHTMLParser(t, resp.Body)
 	csrf := doc.GetCSRF()
 	host, _ := doc.Find(`input[name="host"]`).Attr("value")
-	assert.Equal(t, host, getLDAPServerHost())
+	assert.Equal(t, te.serverHost, host)
 	binddn, _ := doc.Find(`input[name="bind_dn"]`).Attr("value")
 	assert.Equal(t, "uid=gitea,ou=service,dc=planetexpress,dc=com", binddn)
 
-	req = NewRequestWithValues(t, "POST", href, buildAuthSourceLDAPPayload(csrf, "", "", "", "off"))
+	req = NewRequestWithValues(t, "POST", href, te.buildAuthSourcePayload(csrf, ldapAuthOptions{groupTeamMapRemoval: "off"}))
 	session.MakeRequest(t, req, http.StatusSeeOther)
 
 	req = NewRequest(t, "GET", href)
 	resp = session.MakeRequest(t, req, http.StatusOK)
 	doc = NewHTMLParser(t, resp.Body)
 	host, _ = doc.Find(`input[name="host"]`).Attr("value")
-	assert.Equal(t, host, getLDAPServerHost())
+	assert.Equal(t, te.serverHost, host)
 	binddn, _ = doc.Find(`input[name="bind_dn"]`).Attr("value")
 	assert.Equal(t, "uid=gitea,ou=service,dc=planetexpress,dc=com", binddn)
 }
 
 func TestLDAPUserSync(t *testing.T) {
-	if skipLDAPTests() {
-		t.Skip()
+	te := prepareLdapTestEnv(t)
+	if te == nil {
 		return
 	}
+
 	defer tests.PrepareTestEnv(t)()
-	addAuthSourceLDAP(t, "", "")
+	te.addAuthSource(t)
 	err := auth.SyncExternalUsers(context.Background(), true)
 	assert.NoError(t, err)
 
 	// Check if users exists
-	for _, gitLDAPUser := range gitLDAPUsers {
+	for _, gitLDAPUser := range te.gitLDAPUsers {
 		dbUser, err := user_model.GetUserByName(db.DefaultContext, gitLDAPUser.UserName)
 		assert.NoError(t, err)
 		assert.Equal(t, gitLDAPUser.UserName, dbUser.Name)
@@ -238,37 +252,38 @@ func TestLDAPUserSync(t *testing.T) {
 	}
 
 	// Check if no users exist
-	for _, otherLDAPUser := range otherLDAPUsers {
+	for _, otherLDAPUser := range te.otherLDAPUsers {
 		_, err := user_model.GetUserByName(db.DefaultContext, otherLDAPUser.UserName)
 		assert.True(t, user_model.IsErrUserNotExist(err))
 	}
 }
 
 func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) {
-	if skipLDAPTests() {
-		t.Skip()
+	te := prepareLdapTestEnv(t)
+	if te == nil {
 		return
 	}
+
 	defer tests.PrepareTestEnv(t)()
 
 	session := loginUser(t, "user1")
 	csrf := GetUserCSRFToken(t, session)
-	payload := buildAuthSourceLDAPPayload(csrf, "", "", "", "")
+	payload := te.buildAuthSourcePayload(csrf)
 	payload["attribute_username"] = ""
 	req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", payload)
 	session.MakeRequest(t, req, http.StatusSeeOther)
 
-	for _, u := range gitLDAPUsers {
+	for _, u := range te.gitLDAPUsers {
 		req := NewRequest(t, "GET", "/-/admin/users?q="+u.UserName)
 		resp := session.MakeRequest(t, req, http.StatusOK)
 
 		htmlDoc := NewHTMLParser(t, resp.Body)
 
 		tr := htmlDoc.doc.Find("table.table tbody tr")
-		assert.True(t, tr.Length() == 0)
+		assert.Equal(t, 0, tr.Length())
 	}
 
-	for _, u := range gitLDAPUsers {
+	for _, u := range te.gitLDAPUsers {
 		req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{
 			"_csrf":     csrf,
 			"user_name": u.UserName,
@@ -277,7 +292,7 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) {
 		MakeRequest(t, req, http.StatusSeeOther)
 	}
 
-	auth.SyncExternalUsers(context.Background(), true)
+	require.NoError(t, auth.SyncExternalUsers(context.Background(), true))
 
 	authSource := unittest.AssertExistsAndLoadBean(t, &auth_model.Source{
 		Name: payload["name"],
@@ -285,9 +300,9 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) {
 	unittest.AssertCount(t, &user_model.User{
 		LoginType:   auth_model.LDAP,
 		LoginSource: authSource.ID,
-	}, len(gitLDAPUsers))
+	}, len(te.gitLDAPUsers))
 
-	for _, u := range gitLDAPUsers {
+	for _, u := range te.gitLDAPUsers {
 		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
 			Name: u.UserName,
 		})
@@ -296,12 +311,13 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) {
 }
 
 func TestLDAPUserSyncWithGroupFilter(t *testing.T) {
-	if skipLDAPTests() {
-		t.Skip()
+	te := prepareLdapTestEnv(t)
+	if te == nil {
 		return
 	}
+
 	defer tests.PrepareTestEnv(t)()
-	addAuthSourceLDAP(t, "", "(cn=git)")
+	te.addAuthSource(t, ldapAuthOptions{groupFilter: "(cn=git)"})
 
 	// Assert a user not a member of the LDAP group "cn=git" cannot login
 	// This test may look like TestLDAPUserSigninFailed but it is not.
@@ -309,20 +325,20 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) {
 	// This test is for the case when LDAP user records may not be linked with
 	// all groups the user is a member of, the user filter is modified accordingly inside
 	// the addAuthSourceLDAP based on the value of the groupFilter
-	u := otherLDAPUsers[0]
+	u := te.otherLDAPUsers[0]
 	testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect"))
 
-	auth.SyncExternalUsers(context.Background(), true)
+	require.NoError(t, auth.SyncExternalUsers(context.Background(), true))
 
 	// Assert members of LDAP group "cn=git" are added
-	for _, gitLDAPUser := range gitLDAPUsers {
+	for _, gitLDAPUser := range te.gitLDAPUsers {
 		unittest.BeanExists(t, &user_model.User{
 			Name: gitLDAPUser.UserName,
 		})
 	}
 
 	// Assert everyone else is not added
-	for _, gitLDAPUser := range otherLDAPUsers {
+	for _, gitLDAPUser := range te.otherLDAPUsers {
 		unittest.AssertNotExistsBean(t, &user_model.User{
 			Name: gitLDAPUser.UserName,
 		})
@@ -333,11 +349,11 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) {
 	})
 	ldapConfig := ldapSource.Cfg.(*ldap.Source)
 	ldapConfig.GroupFilter = "(cn=ship_crew)"
-	auth_model.UpdateSource(db.DefaultContext, ldapSource)
+	require.NoError(t, auth_model.UpdateSource(db.DefaultContext, ldapSource))
 
-	auth.SyncExternalUsers(context.Background(), true)
+	require.NoError(t, auth.SyncExternalUsers(context.Background(), true))
 
-	for _, gitLDAPUser := range gitLDAPUsers {
+	for _, gitLDAPUser := range te.gitLDAPUsers {
 		if gitLDAPUser.UserName == "fry" || gitLDAPUser.UserName == "leela" || gitLDAPUser.UserName == "bender" {
 			// Assert members of the LDAP group "cn-ship_crew" are still active
 			user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
@@ -355,29 +371,31 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) {
 }
 
 func TestLDAPUserSigninFailed(t *testing.T) {
-	if skipLDAPTests() {
-		t.Skip()
+	te := prepareLdapTestEnv(t)
+	if te == nil {
 		return
 	}
-	defer tests.PrepareTestEnv(t)()
-	addAuthSourceLDAP(t, "", "")
 
-	u := otherLDAPUsers[0]
+	defer tests.PrepareTestEnv(t)()
+	te.addAuthSource(t)
+
+	u := te.otherLDAPUsers[0]
 	testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect"))
 }
 
 func TestLDAPUserSSHKeySync(t *testing.T) {
-	if skipLDAPTests() {
-		t.Skip()
+	te := prepareLdapTestEnv(t)
+	if te == nil {
 		return
 	}
-	defer tests.PrepareTestEnv(t)()
-	addAuthSourceLDAP(t, "sshPublicKey", "")
 
-	auth.SyncExternalUsers(context.Background(), true)
+	defer tests.PrepareTestEnv(t)()
+	te.addAuthSource(t, ldapAuthOptions{attributeSSHPublicKey: "sshPublicKey"})
+
+	require.NoError(t, auth.SyncExternalUsers(context.Background(), true))
 
 	// Check if users has SSH keys synced
-	for _, u := range gitLDAPUsers {
+	for _, u := range te.gitLDAPUsers {
 		if len(u.SSHKeys) == 0 {
 			continue
 		}
@@ -400,18 +418,22 @@ func TestLDAPUserSSHKeySync(t *testing.T) {
 }
 
 func TestLDAPGroupTeamSyncAddMember(t *testing.T) {
-	if skipLDAPTests() {
-		t.Skip()
+	te := prepareLdapTestEnv(t)
+	if te == nil {
 		return
 	}
+
 	defer tests.PrepareTestEnv(t)()
-	addAuthSourceLDAP(t, "", "", "on", `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`)
+	te.addAuthSource(t, ldapAuthOptions{
+		groupTeamMap:        `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`,
+		groupTeamMapRemoval: "on",
+	})
 	org, err := organization.GetOrgByName(db.DefaultContext, "org26")
 	assert.NoError(t, err)
 	team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11")
 	assert.NoError(t, err)
-	auth.SyncExternalUsers(context.Background(), true)
-	for _, gitLDAPUser := range gitLDAPUsers {
+	require.NoError(t, auth.SyncExternalUsers(context.Background(), true))
+	for _, gitLDAPUser := range te.gitLDAPUsers {
 		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
 			Name: gitLDAPUser.UserName,
 		})
@@ -445,19 +467,22 @@ func TestLDAPGroupTeamSyncAddMember(t *testing.T) {
 }
 
 func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
-	if skipLDAPTests() {
-		t.Skip()
+	te := prepareLdapTestEnv(t)
+	if te == nil {
 		return
 	}
 	defer tests.PrepareTestEnv(t)()
-	addAuthSourceLDAP(t, "", "", "on", `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`)
+	te.addAuthSource(t, ldapAuthOptions{
+		groupTeamMap:        `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`,
+		groupTeamMapRemoval: "on",
+	})
 	org, err := organization.GetOrgByName(db.DefaultContext, "org26")
 	assert.NoError(t, err)
 	team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11")
 	assert.NoError(t, err)
-	loginUserWithPassword(t, gitLDAPUsers[0].UserName, gitLDAPUsers[0].Password)
+	loginUserWithPassword(t, te.gitLDAPUsers[0].UserName, te.gitLDAPUsers[0].Password)
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
-		Name: gitLDAPUsers[0].UserName,
+		Name: te.gitLDAPUsers[0].UserName,
 	})
 	err = organization.AddOrgUser(db.DefaultContext, org.ID, user.ID)
 	assert.NoError(t, err)
@@ -470,7 +495,7 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
 	assert.NoError(t, err)
 	assert.True(t, isMember, "User should be member of this team")
 	// assert team member "professor" gets removed from org26 team11
-	loginUserWithPassword(t, gitLDAPUsers[0].UserName, gitLDAPUsers[0].Password)
+	loginUserWithPassword(t, te.gitLDAPUsers[0].UserName, te.gitLDAPUsers[0].Password)
 	isMember, err = organization.IsOrganizationMember(db.DefaultContext, org.ID, user.ID)
 	assert.NoError(t, err)
 	assert.False(t, isMember, "User membership should have been removed from organization")
@@ -480,14 +505,67 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
 }
 
 func TestLDAPPreventInvalidGroupTeamMap(t *testing.T) {
-	if skipLDAPTests() {
-		t.Skip()
+	te := prepareLdapTestEnv(t)
+	if te == nil {
 		return
 	}
 	defer tests.PrepareTestEnv(t)()
 
 	session := loginUser(t, "user1")
 	csrf := GetUserCSRFToken(t, session)
-	req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", buildAuthSourceLDAPPayload(csrf, "", "", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, "off"))
+	payload := te.buildAuthSourcePayload(csrf, ldapAuthOptions{groupTeamMap: `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, groupTeamMapRemoval: "off"})
+	req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", payload)
 	session.MakeRequest(t, req, http.StatusOK) // StatusOK = failed, StatusSeeOther = ok
 }
+
+func TestLDAPEmailSignin(t *testing.T) {
+	te := ldapTestEnv{
+		gitLDAPUsers: []ldapUser{
+			{
+				UserName: "u1",
+				Password: "xx",
+				FullName: "user 1",
+				Email:    "u1@gitea.com",
+			},
+		},
+		serverHost: "mock-host",
+		serverPort: "mock-port",
+	}
+	defer test.MockVariableValue(&ldap.MockedSearchEntry, func(source *ldap.Source, name, passwd string, directBind bool) *ldap.SearchResult {
+		var u *ldapUser
+		for _, user := range te.gitLDAPUsers {
+			if user.Email == name && user.Password == passwd {
+				u = &user
+				break
+			}
+		}
+		if u == nil {
+			return nil
+		}
+		result := &ldap.SearchResult{
+			Username:  u.UserName,
+			Mail:      u.Email,
+			LowerName: strings.ToLower(u.UserName),
+		}
+		nameFields := strings.Split(u.FullName, " ")
+		result.Name = nameFields[0]
+		if len(nameFields) > 1 {
+			result.Surname = nameFields[1]
+		}
+		return result
+	})()
+	defer tests.PrepareTestEnv(t)()
+	te.addAuthSource(t)
+
+	u := te.gitLDAPUsers[0]
+
+	session := loginUserWithPassword(t, u.Email, u.Password)
+	req := NewRequest(t, "GET", "/user/settings")
+	resp := session.MakeRequest(t, req, http.StatusOK)
+
+	htmlDoc := NewHTMLParser(t, resp.Body)
+
+	assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name"))
+	assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name"))
+	assert.Equal(t, u.Email, htmlDoc.Find("#signed-user-email").Text())
+}
diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go
index d1d935da4f..43b151e0b6 100644
--- a/tests/integration/git_helper_for_declarative_test.go
+++ b/tests/integration/git_helper_for_declarative_test.go
@@ -40,10 +40,10 @@ func withKeyFile(t *testing.T, keyname string, callback func(string)) {
 	assert.NoError(t, err)
 
 	// Setup ssh wrapper
-	os.Setenv("GIT_SSH", path.Join(tmpDir, "ssh"))
-	os.Setenv("GIT_SSH_COMMAND",
+	t.Setenv("GIT_SSH", path.Join(tmpDir, "ssh"))
+	t.Setenv("GIT_SSH_COMMAND",
 		"ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i \""+keyFile+"\"")
-	os.Setenv("GIT_SSH_VARIANT", "ssh")
+	t.Setenv("GIT_SSH_VARIANT", "ssh")
 
 	callback(keyFile)
 }
diff --git a/tests/integration/git_push_test.go b/tests/integration/git_push_test.go
index dc0b52203a..e68f8bfce2 100644
--- a/tests/integration/git_push_test.go
+++ b/tests/integration/git_push_test.go
@@ -6,7 +6,6 @@ package integration
 import (
 	"fmt"
 	"net/url"
-	"strings"
 	"testing"
 
 	auth_model "code.gitea.io/gitea/models/auth"
@@ -211,6 +210,6 @@ func TestPushPullRefs(t *testing.T) {
 		})
 		assert.Error(t, err)
 		assert.Empty(t, stdout)
-		assert.False(t, strings.Contains(stderr, "[deleted]"), "stderr: %s", stderr)
+		assert.NotContains(t, stderr, "[deleted]", "stderr: %s", stderr)
 	})
 }
diff --git a/tests/integration/gpg_git_test.go b/tests/integration/gpg_git_test.go
index 047c049c7f..acfe70026e 100644
--- a/tests/integration/gpg_git_test.go
+++ b/tests/integration/gpg_git_test.go
@@ -29,10 +29,7 @@ func TestGPGGit(t *testing.T) {
 	err := os.Chmod(tmpDir, 0o700)
 	assert.NoError(t, err)
 
-	oldGNUPGHome := os.Getenv("GNUPGHOME")
-	err = os.Setenv("GNUPGHOME", tmpDir)
-	assert.NoError(t, err)
-	defer os.Setenv("GNUPGHOME", oldGNUPGHome)
+	t.Setenv("GNUPGHOME", tmpDir)
 
 	// Need to create a root key
 	rootKeyPair, err := importTestingKey()
diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go
index 6b1b6b8b21..9b3b2f2b92 100644
--- a/tests/integration/integration_test.go
+++ b/tests/integration/integration_test.go
@@ -278,7 +278,7 @@ func getTokenForLoggedInUser(t testing.TB, session *TestSession, scopes ...auth.
 	resp = session.MakeRequest(t, req, http.StatusSeeOther)
 
 	// Log the flash values on failure
-	if !assert.Equal(t, resp.Result().Header["Location"], []string{"/user/settings/applications"}) {
+	if !assert.Equal(t, []string{"/user/settings/applications"}, resp.Result().Header["Location"]) {
 		for _, cookie := range resp.Result().Cookies() {
 			if cookie.Name != gitea_context.CookieNameFlash {
 				continue
@@ -453,16 +453,16 @@ func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile
 
 	schemaFilePath := filepath.Join(filepath.Dir(setting.AppPath), "tests", "integration", "schemas", schemaFile)
 	_, schemaFileErr := os.Stat(schemaFilePath)
-	assert.Nil(t, schemaFileErr)
+	assert.NoError(t, schemaFileErr)
 
 	schema, schemaFileReadErr := os.ReadFile(schemaFilePath)
-	assert.Nil(t, schemaFileReadErr)
-	assert.True(t, len(schema) > 0)
+	assert.NoError(t, schemaFileReadErr)
+	assert.NotEmpty(t, schema)
 
 	nodeinfoSchema := gojsonschema.NewStringLoader(string(schema))
 	nodeinfoString := gojsonschema.NewStringLoader(resp.Body.String())
 	result, schemaValidationErr := gojsonschema.Validate(nodeinfoSchema, nodeinfoString)
-	assert.Nil(t, schemaValidationErr)
+	assert.NoError(t, schemaValidationErr)
 	assert.Empty(t, result.Errors())
 	assert.True(t, result.Valid())
 }
diff --git a/tests/integration/migration-test/migration_test.go b/tests/integration/migration-test/migration_test.go
index 627d1f89c4..462cb73eee 100644
--- a/tests/integration/migration-test/migration_test.go
+++ b/tests/integration/migration-test/migration_test.go
@@ -59,7 +59,7 @@ func initMigrationTest(t *testing.T) func() {
 
 	unittest.InitSettings()
 
-	assert.True(t, len(setting.RepoRootPath) != 0)
+	assert.NotEmpty(t, setting.RepoRootPath)
 	assert.NoError(t, unittest.SyncDirs(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
 	assert.NoError(t, git.InitFull(context.Background()))
 	setting.LoadDBSetting()
diff --git a/tests/integration/mirror_push_test.go b/tests/integration/mirror_push_test.go
index 9ff4669bef..0dd8919bff 100644
--- a/tests/integration/mirror_push_test.go
+++ b/tests/integration/mirror_push_test.go
@@ -78,7 +78,7 @@ func testMirrorPush(t *testing.T, u *url.URL) {
 	assert.True(t, doRemovePushMirror(t, session, user.Name, srcRepo.Name, mirrors[0].ID))
 	mirrors, _, err = repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
 	assert.NoError(t, err)
-	assert.Len(t, mirrors, 0)
+	assert.Empty(t, mirrors)
 }
 
 func testCreatePushMirror(t *testing.T, session *TestSession, owner, repo, address, username, password, interval string) {
diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go
index f177bd3a23..d6f1ba33ec 100644
--- a/tests/integration/oauth_test.go
+++ b/tests/integration/oauth_test.go
@@ -89,7 +89,7 @@ func TestAuthorizeRedirectWithExistingGrant(t *testing.T) {
 	u, err := resp.Result().Location()
 	assert.NoError(t, err)
 	assert.Equal(t, "thestate", u.Query().Get("state"))
-	assert.Truef(t, len(u.Query().Get("code")) > 30, "authorization code '%s' should be longer then 30", u.Query().Get("code"))
+	assert.Greaterf(t, len(u.Query().Get("code")), 30, "authorization code '%s' should be longer then 30", u.Query().Get("code"))
 	u.RawQuery = ""
 	assert.Equal(t, "https://example.com/xyzzy", u.String())
 }
@@ -125,8 +125,8 @@ func TestAccessTokenExchange(t *testing.T) {
 	parsed := new(response)
 
 	assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
-	assert.True(t, len(parsed.AccessToken) > 10)
-	assert.True(t, len(parsed.RefreshToken) > 10)
+	assert.Greater(t, len(parsed.AccessToken), 10)
+	assert.Greater(t, len(parsed.RefreshToken), 10)
 }
 
 func TestAccessTokenExchangeWithPublicClient(t *testing.T) {
@@ -148,8 +148,8 @@ func TestAccessTokenExchangeWithPublicClient(t *testing.T) {
 	parsed := new(response)
 
 	assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
-	assert.True(t, len(parsed.AccessToken) > 10)
-	assert.True(t, len(parsed.RefreshToken) > 10)
+	assert.Greater(t, len(parsed.AccessToken), 10)
+	assert.Greater(t, len(parsed.RefreshToken), 10)
 }
 
 func TestAccessTokenExchangeJSON(t *testing.T) {
@@ -172,8 +172,8 @@ func TestAccessTokenExchangeJSON(t *testing.T) {
 	parsed := new(response)
 
 	assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
-	assert.True(t, len(parsed.AccessToken) > 10)
-	assert.True(t, len(parsed.RefreshToken) > 10)
+	assert.Greater(t, len(parsed.AccessToken), 10)
+	assert.Greater(t, len(parsed.RefreshToken), 10)
 }
 
 func TestAccessTokenExchangeWithoutPKCE(t *testing.T) {
@@ -289,8 +289,8 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
 	parsed := new(response)
 
 	assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
-	assert.True(t, len(parsed.AccessToken) > 10)
-	assert.True(t, len(parsed.RefreshToken) > 10)
+	assert.Greater(t, len(parsed.AccessToken), 10)
+	assert.Greater(t, len(parsed.RefreshToken), 10)
 
 	// use wrong client_secret
 	req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
@@ -449,8 +449,8 @@ func TestOAuthIntrospection(t *testing.T) {
 	parsed := new(response)
 
 	assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
-	assert.True(t, len(parsed.AccessToken) > 10)
-	assert.True(t, len(parsed.RefreshToken) > 10)
+	assert.Greater(t, len(parsed.AccessToken), 10)
+	assert.Greater(t, len(parsed.RefreshToken), 10)
 
 	// successful request with a valid client_id/client_secret and a valid token
 	req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
diff --git a/tests/integration/org_count_test.go b/tests/integration/org_count_test.go
index 6386f53f05..8a33c218be 100644
--- a/tests/integration/org_count_test.go
+++ b/tests/integration/org_count_test.go
@@ -130,7 +130,7 @@ func doCheckOrgCounts(username string, orgCounts map[string]int, strict bool, ca
 			calcOrgCounts[org.LowerName] = org.NumRepos
 			count, ok := canonicalCounts[org.LowerName]
 			if ok {
-				assert.True(t, count == org.NumRepos, "Number of Repos in %s is %d when we expected %d", org.Name, org.NumRepos, count)
+				assert.Equal(t, count, org.NumRepos, "Number of Repos in %s is %d when we expected %d", org.Name, org.NumRepos, count)
 			} else {
 				assert.False(t, strict, "Did not expect to see %s with count %d", org.Name, org.NumRepos)
 			}
diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go
index ad0be72dcb..106774aa54 100644
--- a/tests/integration/pull_compare_test.go
+++ b/tests/integration/pull_compare_test.go
@@ -40,7 +40,7 @@ func TestPullCompare(t *testing.T) {
 	resp = session.MakeRequest(t, req, http.StatusOK)
 	doc := NewHTMLParser(t, resp.Body)
 	editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length()
-	assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none")
+	assert.Positive(t, editButtonCount, "Expected to find a button to edit a file in the PR diff view but there were none")
 
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		defer tests.PrepareTestEnv(t)()
@@ -58,7 +58,7 @@ func TestPullCompare(t *testing.T) {
 		resp = session.MakeRequest(t, req, http.StatusOK)
 		doc := NewHTMLParser(t, resp.Body)
 		editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length()
-		assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none")
+		assert.Positive(t, editButtonCount, "Expected to find a button to edit a file in the PR diff view but there were none")
 
 		repoForked := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"})
 		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
@@ -71,7 +71,7 @@ func TestPullCompare(t *testing.T) {
 		resp = session.MakeRequest(t, req, http.StatusOK)
 		doc = NewHTMLParser(t, resp.Body)
 		editButtonCount = doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length()
-		assert.EqualValues(t, editButtonCount, 0, "Expected not to find a button to edit a file in the PR diff view because head repository has been deleted")
+		assert.EqualValues(t, 0, editButtonCount, "Expected not to find a button to edit a file in the PR diff view because head repository has been deleted")
 	})
 }
 
diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go
index eb3743bc17..1521fcfe8a 100644
--- a/tests/integration/pull_merge_test.go
+++ b/tests/integration/pull_merge_test.go
@@ -661,7 +661,7 @@ func TestPullMergeIndexerNotifier(t *testing.T) {
 		searchIssuesResp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
 		var apiIssuesBefore []*api.Issue
 		DecodeJSON(t, searchIssuesResp, &apiIssuesBefore)
-		assert.Len(t, apiIssuesBefore, 0)
+		assert.Empty(t, apiIssuesBefore)
 
 		// merge the pull request
 		elem := strings.Split(test.RedirectURL(createPullResp), "/")
diff --git a/tests/integration/repo_archive_test.go b/tests/integration/repo_archive_test.go
index 664b04baf7..c64ad1193d 100644
--- a/tests/integration/repo_archive_test.go
+++ b/tests/integration/repo_archive_test.go
@@ -29,5 +29,5 @@ func TestRepoDownloadArchive(t *testing.T) {
 	bs, err := io.ReadAll(resp.Body)
 	assert.NoError(t, err)
 	assert.Empty(t, resp.Header().Get("Content-Encoding"))
-	assert.Equal(t, 320, len(bs))
+	assert.Len(t, bs, 320)
 }
diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go
index e7c9853179..267fd0d56e 100644
--- a/tests/integration/repo_fork_test.go
+++ b/tests/integration/repo_fork_test.go
@@ -43,7 +43,7 @@ func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkO
 	link, exists = htmlDoc.doc.Find(`form.ui.form[action*="/fork"]`).Attr("action")
 	assert.True(t, exists, "The template has changed")
 	_, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", forkOwner.ID)).Attr("data-value")
-	assert.True(t, exists, fmt.Sprintf("Fork owner '%s' is not present in select box", forkOwnerName))
+	assert.True(t, exists, "Fork owner '%s' is not present in select box", forkOwnerName)
 	req = NewRequestWithValues(t, "POST", link, map[string]string{
 		"_csrf":              htmlDoc.GetCSRF(),
 		"uid":                fmt.Sprintf("%d", forkOwner.ID),
diff --git a/tests/integration/repo_generate_test.go b/tests/integration/repo_generate_test.go
index 961255cedf..ff2aa220d3 100644
--- a/tests/integration/repo_generate_test.go
+++ b/tests/integration/repo_generate_test.go
@@ -41,7 +41,7 @@ func testRepoGenerate(t *testing.T, session *TestSession, templateID, templateOw
 	link, exists = htmlDoc.doc.Find("form.ui.form[action^=\"/repo/create\"]").Attr("action")
 	assert.True(t, exists, "The template has changed")
 	_, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", generateOwner.ID)).Attr("data-value")
-	assert.True(t, exists, fmt.Sprintf("Generate owner '%s' is not present in select box", generateOwnerName))
+	assert.True(t, exists, "Generate owner '%s' is not present in select box", generateOwnerName)
 	req = NewRequestWithValues(t, "POST", link, map[string]string{
 		"_csrf":         htmlDoc.GetCSRF(),
 		"uid":           fmt.Sprintf("%d", generateOwner.ID),
diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go
index 7889dfaf3b..8c568a1272 100644
--- a/tests/integration/repo_test.go
+++ b/tests/integration/repo_test.go
@@ -228,7 +228,7 @@ func TestViewRepoDirectory(t *testing.T) {
 	repoSummary := htmlDoc.doc.Find(".repository-summary")
 
 	repoFilesTable := htmlDoc.doc.Find("#repo-files-table")
-	assert.NotZero(t, len(repoFilesTable.Nodes))
+	assert.NotEmpty(t, repoFilesTable.Nodes)
 
 	assert.Zero(t, description.Length())
 	assert.Zero(t, repoTopics.Length())
diff --git a/tests/integration/session_test.go b/tests/integration/session_test.go
index d47148efa2..b18a25827d 100644
--- a/tests/integration/session_test.go
+++ b/tests/integration/session_test.go
@@ -28,10 +28,10 @@ func Test_RegenerateSession(t *testing.T) {
 	sess, err := auth.RegenerateSession(db.DefaultContext, "", key)
 	assert.NoError(t, err)
 	assert.EqualValues(t, key, sess.Key)
-	assert.Len(t, sess.Data, 0)
+	assert.Empty(t, sess.Data)
 
 	sess, err = auth.ReadSession(db.DefaultContext, key2)
 	assert.NoError(t, err)
 	assert.EqualValues(t, key2, sess.Key)
-	assert.Len(t, sess.Data, 0)
+	assert.Empty(t, sess.Data)
 }
diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go
index 99e413c6d9..5b6f28d1ff 100644
--- a/tests/integration/user_test.go
+++ b/tests/integration/user_test.go
@@ -255,7 +255,7 @@ func TestListStopWatches(t *testing.T) {
 		assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle)
 		assert.EqualValues(t, repo.Name, apiWatches[0].RepoName)
 		assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName)
-		assert.Greater(t, apiWatches[0].Seconds, int64(0))
+		assert.Positive(t, apiWatches[0].Seconds)
 	}
 }
 
diff --git a/tsconfig.json b/tsconfig.json
index e006535c02..7d0316db29 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,7 +7,8 @@
   ],
   "compilerOptions": {
     "target": "es2020",
-    "module": "nodenext",
+    "module": "esnext",
+    "moduleResolution": "bundler",
     "lib": ["dom", "dom.iterable", "dom.asynciterable", "esnext"],
     "allowImportingTsExtensions": true,
     "allowJs": true,
diff --git a/web_src/css/org.css b/web_src/css/org.css
index 90e5d7ad0e..1082625041 100644
--- a/web_src/css/org.css
+++ b/web_src/css/org.css
@@ -93,11 +93,6 @@
   margin-right: 15px;
 }
 
-.page-content.organization #org-info {
-  overflow-wrap: anywhere;
-  flex: 1;
-}
-
 .page-content.organization #org-info .ui.header {
   display: flex;
   align-items: center;
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 9a43e10e82..6fdc9ec2a8 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -101,11 +101,6 @@
   margin-bottom: 12px;
 }
 
-.repository .repo-description {
-  font-size: 16px;
-  margin-bottom: 5px;
-}
-
 .commit-summary {
   flex: 1;
   overflow-wrap: anywhere;
diff --git a/web_src/css/repo/clone.css b/web_src/css/repo/clone.css
index 15709a78f6..3f6a1323fe 100644
--- a/web_src/css/repo/clone.css
+++ b/web_src/css/repo/clone.css
@@ -1,11 +1,14 @@
 /* only used by "repo/empty.tmpl" */
 .clone-buttons-combo {
+  display: flex;
+  align-items: center;
   flex: 1;
 }
 
 .clone-buttons-combo input {
   border-left: none !important;
   border-radius: 0 !important;
+  height: 30px;
 }
 
 /* used by the clone-panel popup */
diff --git a/web_src/css/repo/home-file-list.css b/web_src/css/repo/home-file-list.css
index eab2124d6f..19ba1f2bcb 100644
--- a/web_src/css/repo/home-file-list.css
+++ b/web_src/css/repo/home-file-list.css
@@ -1,12 +1,23 @@
 #repo-files-table {
   width: 100%;
   display: grid;
-  grid-template-columns: auto 1fr auto;
-  border: 1px solid var(--color-light-border);
+  grid-template-columns: 2fr 3fr auto;
+  border: 1px solid var(--color-secondary);
+  background: var(--color-box-body);
   border-radius: var(--border-radius);
   margin: 10px 0; /* match the "clone-panel-popup" margin to avoid "visual double-border" */
 }
 
+@media (max-width: 767.98px) {
+  #repo-files-table {
+    grid-template-columns: auto 1fr auto;
+  }
+}
+
+#repo-files-table .repo-file-cell.name .svg {
+  margin-right: 2px;
+}
+
 #repo-files-table .svg.octicon-file-directory-fill,
 #repo-files-table .svg.octicon-file-submodule {
   color: var(--color-primary);
@@ -22,18 +33,28 @@
   display: contents;
 }
 
-#repo-files-table .repo-file-item:hover > .repo-file-cell {
-  background: var(--color-hover);
+#repo-files-table .repo-file-item:hover > .repo-file-cell,
+#repo-files-table .parent-link:hover {
+  background: var(--color-hover-opaque);
 }
 
 #repo-files-table .repo-file-line,
 #repo-files-table .repo-file-cell {
-  border-top: 1px solid var(--color-light-border);
-  padding: 6px 10px;
+  border-top: 1px solid var(--color-secondary);
+  padding: 8px 10px;
 }
 
 #repo-files-table .repo-file-line:first-child {
   border-top: none;
+  border-radius: var(--border-radius) var(--border-radius) 0 0;
+}
+
+#repo-files-table .repo-file-item:last-child .repo-file-cell:first-child {
+  border-bottom-left-radius: calc(var(--border-radius) - 1px);
+}
+
+#repo-files-table .repo-file-item:last-child .repo-file-cell:last-child {
+  border-bottom-right-radius: calc(var(--border-radius) - 1px);
 }
 
 #repo-files-table .repo-file-line {
@@ -41,16 +62,24 @@
   display: flex;
   align-items: center;
   gap: 0.5em;
-  padding: 6px 10px;
+}
+
+#repo-files-table .repo-file-last-commit {
+  background: var(--color-box-header);
 }
 
 #repo-files-table .repo-file-cell.name {
-  max-width: 300px;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
 }
 
+@media (max-width: 767.98px) {
+  #repo-files-table .repo-file-cell.name {
+    max-width: 35vw;
+  }
+}
+
 #repo-files-table .repo-file-cell.message {
   white-space: nowrap;
   overflow: hidden;
@@ -59,12 +88,7 @@
 }
 
 #repo-files-table .repo-file-cell.age {
+  text-align: right;
   white-space: nowrap;
   color: var(--color-text-light-1);
 }
-
-@media (max-width: 767.98px) {
-  #repo-files-table .repo-file-cell.name {
-    max-width: 150px;
-  }
-}
diff --git a/web_src/css/repo/home.css b/web_src/css/repo/home.css
index ca5b432804..65005e2263 100644
--- a/web_src/css/repo/home.css
+++ b/web_src/css/repo/home.css
@@ -4,22 +4,24 @@
   grid-template-rows: auto auto 1fr;
 }
 
-.repo-grid-filelist-sidebar .repo-home-filelist {
+.repo-home-filelist {
   min-width: 0;
   grid-column: 1;
   grid-row: 1 / 4;
 }
 
-.repo-grid-filelist-sidebar .repo-home-sidebar-top {
+.repo-home-sidebar-top {
   grid-column: 2;
   grid-row: 1;
   padding-left: 1em;
 }
-.repo-grid-filelist-sidebar .repo-home-sidebar-bottom {
+
+.repo-home-sidebar-bottom {
   grid-column: 2;
   grid-row: 2;
   padding-left: 1em;
 }
+
 .repo-home-sidebar-bottom .flex-list > :first-child {
   border-top: 1px solid var(--color-secondary); /* same to .flex-list > .flex-item + .flex-item */
 }
@@ -29,16 +31,16 @@
     grid-template-columns: 100%;
     grid-template-rows: auto auto auto;
   }
-  .repo-grid-filelist-sidebar .repo-home-filelist {
+  .repo-home-filelist {
     grid-column: 1;
     grid-row: 2;
   }
-  .repo-grid-filelist-sidebar .repo-home-sidebar-top {
+  .repo-home-sidebar-top {
     grid-column: 1;
     grid-row: 1;
     padding-left: 0;
   }
-  .repo-grid-filelist-sidebar .repo-home-sidebar-bottom {
+  .repo-home-sidebar-bottom {
     grid-column: 1;
     grid-row: 3;
     padding-left: 0;
diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css
index 85de7e210a..9bc7747697 100644
--- a/web_src/css/themes/theme-gitea-dark.css
+++ b/web_src/css/themes/theme-gitea-dark.css
@@ -203,6 +203,7 @@
   --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled)));
   --color-light-border: #e8f3ff28;
   --color-hover: #e8f3ff19;
+  --color-hover-opaque: #21252a; /* TODO: color-mix(in srgb, var(--color-body), var(--color-hover)); */
   --color-active: #e8f3ff24;
   --color-menu: #171a1e;
   --color-card: #171a1e;
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index 0bdfd076d6..d7f9debf90 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -203,6 +203,7 @@
   --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled)));
   --color-light-border: #0000171d;
   --color-hover: #00001708;
+  --color-hover-opaque: #f1f3f5; /* TODO: color-mix(in srgb, var(--color-body), var(--color-hover)); */
   --color-active: #00001714;
   --color-menu: #f8f9fb;
   --color-card: #f8f9fb;
diff --git a/web_src/js/components/ActionRunStatus.vue b/web_src/js/components/ActionRunStatus.vue
index 558b881dfe..deab5f6469 100644
--- a/web_src/js/components/ActionRunStatus.vue
+++ b/web_src/js/components/ActionRunStatus.vue
@@ -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>
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index eece2efaf8..cb65a98edd 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -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;
 }
diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts
index 62bfccd139..af9830a4db 100644
--- a/web_src/js/features/codeeditor.ts
+++ b/web_src/js/features/codeeditor.ts
@@ -58,6 +58,12 @@ function initLanguages(monaco: Monaco): void {
     for (const extension of extensions || []) {
       languagesByExt[extension] = id;
     }
+    if (id === 'typescript') {
+      monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
+        // this is needed to suppress error annotations in tsx regarding missing --jsx flag.
+        jsx: monaco.languages.typescript.JsxEmit.Preserve,
+      });
+    }
   }
 }
 
@@ -72,6 +78,8 @@ function updateEditor(monaco: Monaco, editor: IStandaloneCodeEditor, filename: s
   const language = model.getLanguageId();
   const newLanguage = getLanguage(filename);
   if (language !== newLanguage) monaco.editor.setModelLanguage(model, newLanguage);
+  // TODO: Need to update the model uri with the new filename, but there is no easy way currently, see
+  // https://github.com/microsoft/monaco-editor/discussions/3751
 }
 
 // export editor for customization - https://github.com/go-gitea/gitea/issues/10409
@@ -135,10 +143,11 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri
   });
   updateTheme(monaco);
 
+  const model = monaco.editor.createModel(textarea.value, language, monaco.Uri.file(filename));
+
   const editor = monaco.editor.create(container, {
-    value: textarea.value,
+    model,
     theme: 'gitea',
-    language,
     ...other,
   });
 
@@ -146,8 +155,6 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri
     {keybinding: monaco.KeyCode.Enter, command: null}, // disable enter from accepting code completion
   ]);
 
-  const model = editor.getModel();
-  if (!model) throw new Error('Unable to get editor model');
   model.onDidChangeContent(() => {
     textarea.value = editor.getValue({
       preserveBOM: true,
diff --git a/web_src/js/features/common-issue-list.ts b/web_src/js/features/common-issue-list.ts
index e8a47eabad..e207364794 100644
--- a/web_src/js/features/common-issue-list.ts
+++ b/web_src/js/features/common-issue-list.ts
@@ -7,7 +7,7 @@ const reIssueSharpIndex = /^#(\d+)$/; // eg: "#123"
 const reIssueOwnerRepoIndex = /^([-.\w]+)\/([-.\w]+)#(\d+)$/;  // eg: "{owner}/{repo}#{index}"
 
 // if the searchText can be parsed to an "issue goto link", return the link, otherwise return empty string
-export function parseIssueListQuickGotoLink(repoLink, searchText) {
+export function parseIssueListQuickGotoLink(repoLink: string, searchText: string) {
   searchText = searchText.trim();
   let targetUrl = '';
   if (repoLink) {
@@ -15,13 +15,12 @@ export function parseIssueListQuickGotoLink(repoLink, searchText) {
     if (reIssueIndex.test(searchText)) {
       targetUrl = `${repoLink}/issues/${searchText}`;
     } else if (reIssueSharpIndex.test(searchText)) {
-      targetUrl = `${repoLink}/issues/${searchText.substr(1)}`;
+      targetUrl = `${repoLink}/issues/${searchText.substring(1)}`;
     }
   } else {
     // try to parse it for a global search (eg: "owner/repo#123")
-    const matchIssueOwnerRepoIndex = searchText.match(reIssueOwnerRepoIndex);
-    if (matchIssueOwnerRepoIndex) {
-      const [_, owner, repo, index] = matchIssueOwnerRepoIndex;
+    const [_, owner, repo, index] = reIssueOwnerRepoIndex.exec(searchText) || [];
+    if (owner) {
       targetUrl = `${appSubUrl}/${owner}/${repo}/issues/${index}`;
     }
   }
@@ -33,7 +32,7 @@ export function initCommonIssueListQuickGoto() {
   if (!goto) return;
 
   const form = goto.closest('form');
-  const input = form.querySelector('input[name=q]');
+  const input = form.querySelector<HTMLInputElement>('input[name=q]');
   const repoLink = goto.getAttribute('data-repo-link');
 
   form.addEventListener('submit', (e) => {
diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts
index 80eabaa37a..bba50a1296 100644
--- a/web_src/js/features/comp/ComboMarkdownEditor.ts
+++ b/web_src/js/features/comp/ComboMarkdownEditor.ts
@@ -283,8 +283,8 @@ export class ComboMarkdownEditor {
     ];
   }
 
-  parseEasyMDEToolbar(EasyMDE, actions) {
-    this.easyMDEToolbarActions = this.easyMDEToolbarActions || easyMDEToolbarActions(EasyMDE, this);
+  parseEasyMDEToolbar(easyMde: typeof EasyMDE, actions) {
+    this.easyMDEToolbarActions = this.easyMDEToolbarActions || easyMDEToolbarActions(easyMde, this);
     const processed = [];
     for (const action of actions) {
       const actionButton = this.easyMDEToolbarActions[action];
diff --git a/web_src/js/features/comp/EasyMDEToolbarActions.ts b/web_src/js/features/comp/EasyMDEToolbarActions.ts
index d91dd23d11..ec5c7304be 100644
--- a/web_src/js/features/comp/EasyMDEToolbarActions.ts
+++ b/web_src/js/features/comp/EasyMDEToolbarActions.ts
@@ -1,100 +1,102 @@
 import {svg} from '../../svg.ts';
+import type EasyMDE from 'easymde';
+import type {ComboMarkdownEditor} from './ComboMarkdownEditor.ts';
 
-export function easyMDEToolbarActions(EasyMDE, editor) {
-  const actions = {
+export function easyMDEToolbarActions(easyMde: typeof EasyMDE, editor: ComboMarkdownEditor): Record<string, Partial<EasyMDE.ToolbarIcon | string>> {
+  const actions: Record<string, Partial<EasyMDE.ToolbarIcon> | string> = {
     '|': '|',
     'heading-1': {
-      action: EasyMDE.toggleHeading1,
+      action: easyMde.toggleHeading1,
       icon: svg('octicon-heading'),
       title: 'Heading 1',
     },
     'heading-2': {
-      action: EasyMDE.toggleHeading2,
+      action: easyMde.toggleHeading2,
       icon: svg('octicon-heading'),
       title: 'Heading 2',
     },
     'heading-3': {
-      action: EasyMDE.toggleHeading3,
+      action: easyMde.toggleHeading3,
       icon: svg('octicon-heading'),
       title: 'Heading 3',
     },
     'heading-smaller': {
-      action: EasyMDE.toggleHeadingSmaller,
+      action: easyMde.toggleHeadingSmaller,
       icon: svg('octicon-heading'),
       title: 'Decrease Heading',
     },
     'heading-bigger': {
-      action: EasyMDE.toggleHeadingBigger,
+      action: easyMde.toggleHeadingBigger,
       icon: svg('octicon-heading'),
       title: 'Increase Heading',
     },
     'bold': {
-      action: EasyMDE.toggleBold,
+      action: easyMde.toggleBold,
       icon: svg('octicon-bold'),
       title: 'Bold',
     },
     'italic': {
-      action: EasyMDE.toggleItalic,
+      action: easyMde.toggleItalic,
       icon: svg('octicon-italic'),
       title: 'Italic',
     },
     'strikethrough': {
-      action: EasyMDE.toggleStrikethrough,
+      action: easyMde.toggleStrikethrough,
       icon: svg('octicon-strikethrough'),
       title: 'Strikethrough',
     },
     'quote': {
-      action: EasyMDE.toggleBlockquote,
+      action: easyMde.toggleBlockquote,
       icon: svg('octicon-quote'),
       title: 'Quote',
     },
     'code': {
-      action: EasyMDE.toggleCodeBlock,
+      action: easyMde.toggleCodeBlock,
       icon: svg('octicon-code'),
       title: 'Code',
     },
     'link': {
-      action: EasyMDE.drawLink,
+      action: easyMde.drawLink,
       icon: svg('octicon-link'),
       title: 'Link',
     },
     'unordered-list': {
-      action: EasyMDE.toggleUnorderedList,
+      action: easyMde.toggleUnorderedList,
       icon: svg('octicon-list-unordered'),
       title: 'Unordered List',
     },
     'ordered-list': {
-      action: EasyMDE.toggleOrderedList,
+      action: easyMde.toggleOrderedList,
       icon: svg('octicon-list-ordered'),
       title: 'Ordered List',
     },
     'image': {
-      action: EasyMDE.drawImage,
+      action: easyMde.drawImage,
       icon: svg('octicon-image'),
       title: 'Image',
     },
     'table': {
-      action: EasyMDE.drawTable,
+      action: easyMde.drawTable,
       icon: svg('octicon-table'),
       title: 'Table',
     },
     'horizontal-rule': {
-      action: EasyMDE.drawHorizontalRule,
+      action: easyMde.drawHorizontalRule,
       icon: svg('octicon-horizontal-rule'),
       title: 'Horizontal Rule',
     },
     'preview': {
-      action: EasyMDE.togglePreview,
+      action: easyMde.togglePreview,
       icon: svg('octicon-eye'),
       title: 'Preview',
     },
     'fullscreen': {
-      action: EasyMDE.toggleFullScreen,
+      action: easyMde.toggleFullScreen,
       icon: svg('octicon-screen-full'),
       title: 'Fullscreen',
     },
     'side-by-side': {
-      action: EasyMDE.toggleSideBySide,
+      action: easyMde.toggleSideBySide,
       icon: svg('octicon-columns'),
       title: 'Side by Side',
     },
diff --git a/web_src/js/features/comp/ReactionSelector.ts b/web_src/js/features/comp/ReactionSelector.ts
index 1e955c7ab4..671bade3be 100644
--- a/web_src/js/features/comp/ReactionSelector.ts
+++ b/web_src/js/features/comp/ReactionSelector.ts
@@ -3,7 +3,7 @@ import {fomanticQuery} from '../../modules/fomantic/base.ts';
 
 export function initCompReactionSelector(parent: ParentNode = document) {
   for (const container of parent.querySelectorAll('.issue-content, .diff-file-body')) {
-    container.addEventListener('click', async (e) => {
+    container.addEventListener('click', async (e: MouseEvent & {target: HTMLElement}) => {
       // there are 2 places for the "reaction" buttons, one is the top-right reaction menu, one is the bottom of the comment
       const target = e.target.closest('.comment-reaction-button');
       if (!target) return;
diff --git a/web_src/js/features/comp/WebHookEditor.ts b/web_src/js/features/comp/WebHookEditor.ts
index b13a2ffca3..203396af80 100644
--- a/web_src/js/features/comp/WebHookEditor.ts
+++ b/web_src/js/features/comp/WebHookEditor.ts
@@ -23,7 +23,7 @@ export function initCompWebHookEditor() {
   }
 
   // some webhooks (like Gitea) allow to set the request method (GET/POST), and it would toggle the "Content Type" field
-  const httpMethodInput = document.querySelector('#http_method');
+  const httpMethodInput = document.querySelector<HTMLInputElement>('#http_method');
   if (httpMethodInput) {
     const updateContentType = function () {
       const visible = httpMethodInput.value === 'POST';
diff --git a/web_src/js/features/dropzone.ts b/web_src/js/features/dropzone.ts
index c9b0149df5..666c645230 100644
--- a/web_src/js/features/dropzone.ts
+++ b/web_src/js/features/dropzone.ts
@@ -6,6 +6,7 @@ import {GET, POST} from '../modules/fetch.ts';
 import {showErrorToast} from '../modules/toast.ts';
 import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.ts';
 import {isImageFile, isVideoFile} from '../utils.ts';
+import type {DropzoneFile} from 'dropzone/index.js';
 
 const {csrfToken, i18n} = window.config;
 
@@ -15,14 +16,14 @@ export const DropzoneCustomEventRemovedFile = 'dropzone-custom-removed-file';
 export const DropzoneCustomEventUploadDone = 'dropzone-custom-upload-done';
 
 async function createDropzone(el, opts) {
-  const [{Dropzone}] = await Promise.all([
+  const [{default: Dropzone}] = await Promise.all([
     import(/* webpackChunkName: "dropzone" */'dropzone'),
     import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'),
   ]);
   return new Dropzone(el, opts);
 }
 
-export function generateMarkdownLinkForAttachment(file, {width, dppx} = {}) {
+export function generateMarkdownLinkForAttachment(file, {width, dppx}: {width?: number, dppx?: number} = {}) {
   let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`;
   if (isImageFile(file)) {
     fileMarkdown = `!${fileMarkdown}`;
@@ -60,14 +61,14 @@ function addCopyLink(file) {
 /**
  * @param {HTMLElement} dropzoneEl
  */
-export async function initDropzone(dropzoneEl) {
+export async function initDropzone(dropzoneEl: HTMLElement) {
   const listAttachmentsUrl = dropzoneEl.closest('[data-attachment-url]')?.getAttribute('data-attachment-url');
   const removeAttachmentUrl = dropzoneEl.getAttribute('data-remove-url');
   const attachmentBaseLinkUrl = dropzoneEl.getAttribute('data-link-url');
 
   let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
   let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
-  const opts = {
+  const opts: Record<string, any> = {
     url: dropzoneEl.getAttribute('data-upload-url'),
     headers: {'X-Csrf-Token': csrfToken},
     acceptedFiles: ['*/*', ''].includes(dropzoneEl.getAttribute('data-accepts')) ? null : dropzoneEl.getAttribute('data-accepts'),
@@ -88,7 +89,7 @@ export async function initDropzone(dropzoneEl) {
   // "http://localhost:3000/owner/repo/issues/[object%20Event]"
   // the reason is that the preview "callback(dataURL)" is assign to "img.onerror" then "thumbnail" uses the error object as the dataURL and generates '<img src="[object Event]">'
   const dzInst = await createDropzone(dropzoneEl, opts);
-  dzInst.on('success', (file, resp) => {
+  dzInst.on('success', (file: DropzoneFile & {uuid: string}, resp: any) => {
     file.uuid = resp.uuid;
     fileUuidDict[file.uuid] = {submitted: false};
     const input = createElementFromAttrs('input', {name: 'files', type: 'hidden', id: `dropzone-file-${resp.uuid}`, value: resp.uuid});
@@ -97,7 +98,7 @@ export async function initDropzone(dropzoneEl) {
     dzInst.emit(DropzoneCustomEventUploadDone, {file});
   });
 
-  dzInst.on('removedfile', async (file) => {
+  dzInst.on('removedfile', async (file: DropzoneFile & {uuid: string}) => {
     if (disableRemovedfileEvent) return;
 
     dzInst.emit(DropzoneCustomEventRemovedFile, {fileUuid: file.uuid});
diff --git a/web_src/js/features/emoji.ts b/web_src/js/features/emoji.ts
index 032a3efe8a..933aa951c5 100644
--- a/web_src/js/features/emoji.ts
+++ b/web_src/js/features/emoji.ts
@@ -1,4 +1,4 @@
-import emojis from '../../../assets/emoji.json';
+import emojis from '../../../assets/emoji.json' with {type: 'json'};
 
 const {assetUrlPrefix, customEmojis} = window.config;
 
diff --git a/web_src/js/features/eventsource.sharedworker.ts b/web_src/js/features/eventsource.sharedworker.ts
index 62581cf687..991c92cc8e 100644
--- a/web_src/js/features/eventsource.sharedworker.ts
+++ b/web_src/js/features/eventsource.sharedworker.ts
@@ -2,6 +2,11 @@ const sourcesByUrl = {};
 const sourcesByPort = {};
 
 class Source {
+  url: string;
+  eventSource: EventSource;
+  listening: Record<string, any>;
+  clients: Array<any>;
+
   constructor(url) {
     this.url = url;
     this.eventSource = new EventSource(url);
@@ -67,7 +72,7 @@ class Source {
   }
 }
 
-self.addEventListener('connect', (e) => {
+self.addEventListener('connect', (e: Event & {ports: Array<any>}) => {
   for (const port of e.ports) {
     port.addEventListener('message', (event) => {
       if (!self.EventSource) {
diff --git a/web_src/js/features/heatmap.ts b/web_src/js/features/heatmap.ts
index 69cd069a94..53eebc93e5 100644
--- a/web_src/js/features/heatmap.ts
+++ b/web_src/js/features/heatmap.ts
@@ -21,8 +21,8 @@ export function initHeatmap() {
     // last heatmap tooltip localization attempt https://github.com/go-gitea/gitea/pull/24131/commits/a83761cbbae3c2e3b4bced71e680f44432073ac8
     const locale = {
       heatMapLocale: {
-        months: new Array(12).fill().map((_, idx) => translateMonth(idx)),
-        days: new Array(7).fill().map((_, idx) => translateDay(idx)),
+        months: new Array(12).fill(undefined).map((_, idx) => translateMonth(idx)),
+        days: new Array(7).fill(undefined).map((_, idx) => translateDay(idx)),
         on: ' - ', // no correct locale support for it, because in many languages the sentence is not "something on someday"
         more: el.getAttribute('data-locale-more'),
         less: el.getAttribute('data-locale-less'),
diff --git a/web_src/js/features/install.ts b/web_src/js/features/install.ts
index 3defb7904a..725dcafab0 100644
--- a/web_src/js/features/install.ts
+++ b/web_src/js/features/install.ts
@@ -22,9 +22,9 @@ function initPreInstall() {
     mssql: '127.0.0.1:1433',
   };
 
-  const dbHost = document.querySelector('#db_host');
-  const dbUser = document.querySelector('#db_user');
-  const dbName = document.querySelector('#db_name');
+  const dbHost = document.querySelector<HTMLInputElement>('#db_host');
+  const dbUser = document.querySelector<HTMLInputElement>('#db_user');
+  const dbName = document.querySelector<HTMLInputElement>('#db_name');
 
   // Database type change detection.
   document.querySelector('#db_type').addEventListener('change', function () {
@@ -48,12 +48,12 @@ function initPreInstall() {
   });
   document.querySelector('#db_type').dispatchEvent(new Event('change'));
 
-  const appUrl = document.querySelector('#app_url');
+  const appUrl = document.querySelector<HTMLInputElement>('#app_url');
   if (appUrl.value.includes('://localhost')) {
     appUrl.value = window.location.href;
   }
 
-  const domain = document.querySelector('#domain');
+  const domain = document.querySelector<HTMLInputElement>('#domain');
   if (domain.value.trim() === 'localhost') {
     domain.value = window.location.hostname;
   }
@@ -61,43 +61,43 @@ function initPreInstall() {
   // TODO: better handling of exclusive relations.
   document.querySelector('#offline-mode input').addEventListener('change', function () {
     if (this.checked) {
-      document.querySelector('#disable-gravatar input').checked = true;
-      document.querySelector('#federated-avatar-lookup input').checked = false;
+      document.querySelector<HTMLInputElement>('#disable-gravatar input').checked = true;
+      document.querySelector<HTMLInputElement>('#federated-avatar-lookup input').checked = false;
     }
   });
   document.querySelector('#disable-gravatar input').addEventListener('change', function () {
     if (this.checked) {
-      document.querySelector('#federated-avatar-lookup input').checked = false;
+      document.querySelector<HTMLInputElement>('#federated-avatar-lookup input').checked = false;
     } else {
-      document.querySelector('#offline-mode input').checked = false;
+      document.querySelector<HTMLInputElement>('#offline-mode input').checked = false;
     }
   });
   document.querySelector('#federated-avatar-lookup input').addEventListener('change', function () {
     if (this.checked) {
-      document.querySelector('#disable-gravatar input').checked = false;
-      document.querySelector('#offline-mode input').checked = false;
+      document.querySelector<HTMLInputElement>('#disable-gravatar input').checked = false;
+      document.querySelector<HTMLInputElement>('#offline-mode input').checked = false;
     }
   });
   document.querySelector('#enable-openid-signin input').addEventListener('change', function () {
     if (this.checked) {
-      if (!document.querySelector('#disable-registration input').checked) {
-        document.querySelector('#enable-openid-signup input').checked = true;
+      if (!document.querySelector<HTMLInputElement>('#disable-registration input').checked) {
+        document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = true;
       }
     } else {
-      document.querySelector('#enable-openid-signup input').checked = false;
+      document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = false;
     }
   });
   document.querySelector('#disable-registration input').addEventListener('change', function () {
     if (this.checked) {
-      document.querySelector('#enable-captcha input').checked = false;
-      document.querySelector('#enable-openid-signup input').checked = false;
+      document.querySelector<HTMLInputElement>('#enable-captcha input').checked = false;
+      document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = false;
     } else {
-      document.querySelector('#enable-openid-signup input').checked = true;
+      document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = true;
     }
   });
   document.querySelector('#enable-captcha input').addEventListener('change', function () {
     if (this.checked) {
-      document.querySelector('#disable-registration input').checked = false;
+      document.querySelector<HTMLInputElement>('#disable-registration input').checked = false;
     }
   });
 }
diff --git a/web_src/js/features/notification.ts b/web_src/js/features/notification.ts
index 539f779056..5cdcd967f0 100644
--- a/web_src/js/features/notification.ts
+++ b/web_src/js/features/notification.ts
@@ -14,25 +14,25 @@ export function initNotificationsTable() {
   window.addEventListener('pageshow', (e) => {
     if (e.persisted) { // page was restored from bfcache
       const table = document.querySelector('#notification_table');
-      const unreadCountEl = document.querySelector('.notifications-unread-count');
+      const unreadCountEl = document.querySelector<HTMLElement>('.notifications-unread-count');
       let unreadCount = parseInt(unreadCountEl.textContent);
       for (const item of table.querySelectorAll('.notifications-item[data-remove="true"]')) {
         item.remove();
         unreadCount -= 1;
       }
-      unreadCountEl.textContent = unreadCount;
+      unreadCountEl.textContent = String(unreadCount);
     }
   });
 
   // mark clicked unread links for deletion on bfcache restore
   for (const link of table.querySelectorAll('.notifications-item[data-status="1"] .notifications-link')) {
-    link.addEventListener('click', (e) => {
+    link.addEventListener('click', (e : MouseEvent & {target: HTMLElement}) => {
       e.target.closest('.notifications-item').setAttribute('data-remove', 'true');
     });
   }
 }
 
-async function receiveUpdateCount(event) {
+async function receiveUpdateCount(event: MessageEvent) {
   try {
     const data = JSON.parse(event.data);
 
@@ -50,7 +50,7 @@ export function initNotificationCount() {
   if (!document.querySelector('.notification_count')) return;
 
   let usingPeriodicPoller = false;
-  const startPeriodicPoller = (timeout, lastCount) => {
+  const startPeriodicPoller = (timeout: number, lastCount?: number) => {
     if (timeout <= 0 || !Number.isFinite(timeout)) return;
     usingPeriodicPoller = true;
     lastCount = lastCount ?? getCurrentCount();
@@ -72,13 +72,13 @@ export function initNotificationCount() {
       type: 'start',
       url: `${window.location.origin}${appSubUrl}/user/events`,
     });
-    worker.port.addEventListener('message', (event) => {
+    worker.port.addEventListener('message', (event: MessageEvent) => {
       if (!event.data || !event.data.type) {
         console.error('unknown worker message event', event);
         return;
       }
       if (event.data.type === 'notification-count') {
-        const _promise = receiveUpdateCount(event.data);
+        receiveUpdateCount(event); // no await
       } else if (event.data.type === 'no-event-source') {
         // browser doesn't support EventSource, falling back to periodic poller
         if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout);
@@ -118,10 +118,10 @@ export function initNotificationCount() {
 }
 
 function getCurrentCount() {
-  return document.querySelector('.notification_count').textContent;
+  return Number(document.querySelector('.notification_count').textContent ?? '0');
 }
 
-async function updateNotificationCountWithCallback(callback, timeout, lastCount) {
+async function updateNotificationCountWithCallback(callback: (timeout: number, newCount: number) => void, timeout: number, lastCount: number) {
   const currentCount = getCurrentCount();
   if (lastCount !== currentCount) {
     callback(notificationSettings.MinTimeout, currentCount);
@@ -149,10 +149,9 @@ async function updateNotificationTable() {
   if (notificationDiv) {
     try {
       const params = new URLSearchParams(window.location.search);
-      params.set('div-only', true);
-      params.set('sequence-number', ++notificationSequenceNumber);
-      const url = `${appSubUrl}/notifications?${params.toString()}`;
-      const response = await GET(url);
+      params.set('div-only', String(true));
+      params.set('sequence-number', String(++notificationSequenceNumber));
+      const response = await GET(`${appSubUrl}/notifications?${params.toString()}`);
 
       if (!response.ok) {
         throw new Error('Failed to fetch notification table');
@@ -169,7 +168,7 @@ async function updateNotificationTable() {
   }
 }
 
-async function updateNotificationCount() {
+async function updateNotificationCount(): Promise<number> {
   try {
     const response = await GET(`${appSubUrl}/notifications/new`);
 
@@ -185,9 +184,9 @@ async function updateNotificationCount() {
       el.textContent = `${data.new}`;
     }
 
-    return `${data.new}`;
+    return data.new as number;
   } catch (error) {
     console.error(error);
-    return '0';
+    return 0;
   }
 }
diff --git a/web_src/js/features/oauth2-settings.ts b/web_src/js/features/oauth2-settings.ts
index 1e62ca0096..a206bc8912 100644
--- a/web_src/js/features/oauth2-settings.ts
+++ b/web_src/js/features/oauth2-settings.ts
@@ -1,5 +1,7 @@
 export function initOAuth2SettingsDisableCheckbox() {
-  for (const e of document.querySelectorAll('.disable-setting')) e.addEventListener('change', ({target}) => {
-    document.querySelector(e.getAttribute('data-target')).classList.toggle('disabled', target.checked);
-  });
+  for (const el of document.querySelectorAll('.disable-setting')) {
+    el.addEventListener('change', (e: Event & {target: HTMLInputElement}) => {
+      document.querySelector(e.target.getAttribute('data-target')).classList.toggle('disabled', e.target.checked);
+    });
+  }
 }
diff --git a/web_src/js/features/pull-view-file.ts b/web_src/js/features/pull-view-file.ts
index 9a052207d5..36fe4bc4df 100644
--- a/web_src/js/features/pull-view-file.ts
+++ b/web_src/js/features/pull-view-file.ts
@@ -34,7 +34,7 @@ export function countAndUpdateViewedFiles() {
 export function initViewedCheckboxListenerFor() {
   for (const form of document.querySelectorAll(`${viewedCheckboxSelector}:not([data-has-viewed-checkbox-listener="true"])`)) {
     // To prevent double addition of listeners
-    form.setAttribute('data-has-viewed-checkbox-listener', true);
+    form.setAttribute('data-has-viewed-checkbox-listener', String(true));
 
     // The checkbox consists of a div containing the real checkbox with its label and the CSRF token,
     // hence the actual checkbox first has to be found
@@ -67,7 +67,7 @@ export function initViewedCheckboxListenerFor() {
       // Unfortunately, actual forms cause too many problems, hence another approach is needed
       const files = {};
       files[fileName] = this.checked;
-      const data = {files};
+      const data: Record<string, any> = {files};
       const headCommitSHA = form.getAttribute('data-headcommit');
       if (headCommitSHA) data.headCommitSHA = headCommitSHA;
       POST(form.getAttribute('data-link'), {data});
diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts
index 96b08250fb..32d0b84f4c 100644
--- a/web_src/js/features/repo-editor.ts
+++ b/web_src/js/features/repo-editor.ts
@@ -35,7 +35,7 @@ function initEditPreviewTab(elForm: HTMLFormElement) {
 }
 
 export function initRepoEditor() {
-  const dropzoneUpload = document.querySelector('.page-content.repository.editor.upload .dropzone');
+  const dropzoneUpload = document.querySelector<HTMLElement>('.page-content.repository.editor.upload .dropzone');
   if (dropzoneUpload) initDropzone(dropzoneUpload);
 
   const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
diff --git a/web_src/js/features/repo-home.ts b/web_src/js/features/repo-home.ts
index 4c69a00434..abda29cc52 100644
--- a/web_src/js/features/repo-home.ts
+++ b/web_src/js/features/repo-home.ts
@@ -16,7 +16,7 @@ export function initRepoTopicBar() {
   let lastErrorToast: Toast;
 
   mgrBtn.addEventListener('click', () => {
-    hideElem(viewDiv);
+    hideElem([viewDiv, mgrBtn]);
     showElem(editDiv);
     topicDropdown.querySelector<HTMLInputElement>('input.search').focus();
   });
@@ -24,7 +24,7 @@ export function initRepoTopicBar() {
   document.querySelector('#cancel_topic_edit').addEventListener('click', () => {
     lastErrorToast?.hideToast();
     hideElem(editDiv);
-    showElem(viewDiv);
+    showElem([viewDiv, mgrBtn]);
     mgrBtn.focus();
   });
 
@@ -55,7 +55,7 @@ export function initRepoTopicBar() {
           }
         }
         hideElem(editDiv);
-        showElem(viewDiv);
+        showElem([viewDiv, mgrBtn]);
       }
     } else if (response.status === 422) {
       // how to test: input topic like " invalid topic " (with spaces), and select it from the list, then "Save"
diff --git a/web_src/js/features/repo-issue-sidebar.ts b/web_src/js/features/repo-issue-sidebar.ts
index 45cd38d533..ef2b7d143c 100644
--- a/web_src/js/features/repo-issue-sidebar.ts
+++ b/web_src/js/features/repo-issue-sidebar.ts
@@ -4,6 +4,7 @@ import {queryElems, toggleElem} from '../utils/dom.ts';
 import {initIssueSidebarComboList} from './repo-issue-sidebar-combolist.ts';
 
 function initBranchSelector() {
+  // TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl"
   const elSelectBranch = document.querySelector('.ui.dropdown.select-branch');
   if (!elSelectBranch) return;
 
diff --git a/web_src/js/features/repo-search.ts b/web_src/js/features/repo-search.ts
index 9cc2dd4223..7f111dce33 100644
--- a/web_src/js/features/repo-search.ts
+++ b/web_src/js/features/repo-search.ts
@@ -5,9 +5,10 @@ export function initRepositorySearch() {
   repositorySearchForm.addEventListener('change', (e: Event & {target: HTMLFormElement}) => {
     e.preventDefault();
 
-    const formData = new FormData(repositorySearchForm);
-    const params = new URLSearchParams(formData);
-
+    const params = new URLSearchParams();
+    for (const [key, value] of new FormData(repositorySearchForm).entries()) {
+      params.set(key, value.toString());
+    }
     if (e.target.name === 'clear-filter') {
       params.delete('archived');
       params.delete('fork');
diff --git a/web_src/js/features/repo-settings-branches.test.ts b/web_src/js/features/repo-settings-branches.test.ts
index c4609999be..32ab54e4c2 100644
--- a/web_src/js/features/repo-settings-branches.test.ts
+++ b/web_src/js/features/repo-settings-branches.test.ts
@@ -2,6 +2,7 @@ import {beforeEach, describe, expect, test, vi} from 'vitest';
 import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
 import {POST} from '../modules/fetch.ts';
 import {createSortable} from '../modules/sortable.ts';
+import type {SortableEvent} from 'sortablejs';
 
 vi.mock('../modules/fetch.ts', () => ({
   POST: vi.fn(),
@@ -54,8 +55,8 @@ describe('Repository Branch Settings', () => {
     vi.mocked(POST).mockResolvedValue({ok: true} as Response);
 
     // Mock createSortable to capture and execute the onEnd callback
-    vi.mocked(createSortable).mockImplementation((_el, options) => {
-      options.onEnd();
+    vi.mocked(createSortable).mockImplementation(async (_el: Element, options) => {
+      options.onEnd(new Event('SortableEvent') as SortableEvent);
       return {destroy: vi.fn()};
     });
 
diff --git a/web_src/js/features/tribute.ts b/web_src/js/features/tribute.ts
index 44588c0064..fa65bcbb28 100644
--- a/web_src/js/features/tribute.ts
+++ b/web_src/js/features/tribute.ts
@@ -51,6 +51,7 @@ function makeCollections({mentions, emoji}) {
 export async function attachTribute(element, {mentions, emoji}) {
   const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs');
   const collections = makeCollections({mentions, emoji});
+  // @ts-expect-error TS2351: This expression is not constructable (strange, why)
   const tribute = new Tribute({collection: collections, noMatchTemplate: ''});
   tribute.attach(element);
   return tribute;
diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts
index 9780a1cf3c..a5ec29a83f 100644
--- a/web_src/js/globals.d.ts
+++ b/web_src/js/globals.d.ts
@@ -8,6 +8,17 @@ declare module '*.css' {
   export default value;
 }
 
+declare module '*.vue' {
+  import type {DefineComponent} from 'vue';
+  const component: DefineComponent<unknown, unknown, any>;
+  export default component;
+  // List of named exports from vue components, used to make `tsc` output clean.
+  // To actually lint .vue files, `vue-tsc` is used because `tsc` can not parse them.
+  export function initRepoBranchTagSelector(selector: string): void;
+  export function initDashboardRepoList(): void;
+  export function initRepositoryActionView(): void;
+}
+
 declare let __webpack_public_path__: string;
 
 declare module 'htmx.org/dist/htmx.esm.js' {
@@ -16,8 +27,8 @@ declare module 'htmx.org/dist/htmx.esm.js' {
 }
 
 declare module 'uint8-to-base64' {
-  export function encode(arrayBuffer: ArrayBuffer): string;
-  export function decode(base64str: string): ArrayBuffer;
+  export function encode(arrayBuffer: Uint8Array): string;
+  export function decode(base64str: string): Uint8Array;
 }
 
 declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' {
diff --git a/web_src/js/markup/math.ts b/web_src/js/markup/math.ts
index 6a1ca2f2e3..4777805e3c 100644
--- a/web_src/js/markup/math.ts
+++ b/web_src/js/markup/math.ts
@@ -1,9 +1,14 @@
 import {displayError} from './common.ts';
 
-function targetElement(el: Element) {
-  // The target element is either the current element if it has the
-  // `is-loading` class or the pre that contains it
-  return el.classList.contains('is-loading') ? el : el.closest('pre');
+function targetElement(el: Element): {target: Element, displayAsBlock: boolean} {
+  // The target element is either the parent "code block with loading indicator", or itself
+  // It is designed to work for 2 cases (guaranteed by backend code):
+  // * <pre class="code-block is-loading"><code class="language-math display">...</code></pre>
+  // * <code class="language-math">...</code>
+  return {
+    target: el.closest('.code-block.is-loading') ?? el,
+    displayAsBlock: el.classList.contains('display'),
+  };
 }
 
 export async function renderMath(): Promise<void> {
@@ -20,7 +25,7 @@ export async function renderMath(): Promise<void> {
   const MAX_EXPAND = 1000;
 
   for (const el of els) {
-    const target = targetElement(el);
+    const {target, displayAsBlock} = targetElement(el);
     if (target.hasAttribute('data-render-done')) continue;
     const source = el.textContent;
 
@@ -28,16 +33,12 @@ export async function renderMath(): Promise<void> {
       displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`));
       continue;
     }
-
-    const displayMode = el.classList.contains('display');
-    const nodeName = displayMode ? 'p' : 'span';
-
     try {
-      const tempEl = document.createElement(nodeName);
+      const tempEl = document.createElement(displayAsBlock ? 'p' : 'span');
       katex.render(source, tempEl, {
         maxSize: MAX_SIZE,
         maxExpand: MAX_EXPAND,
-        displayMode,
+        displayMode: displayAsBlock, // katex: true for display (block) mode, false for inline mode
       });
       target.replaceWith(tempEl);
     } catch (error) {
diff --git a/web_src/js/modules/tippy.ts b/web_src/js/modules/tippy.ts
index ce0b3cbc39..4e7f1ac093 100644
--- a/web_src/js/modules/tippy.ts
+++ b/web_src/js/modules/tippy.ts
@@ -16,7 +16,6 @@ export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
   // because we should use our own wrapper functions to handle them, do not let the user override them
   const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
 
-  // @ts-expect-error: wrong type derived by typescript
   const instance: Instance = tippy(target, {
     appendTo: document.body,
     animation: false,
diff --git a/web_src/js/svg.ts b/web_src/js/svg.ts
index 3a0f2ed53c..90b12fa87d 100644
--- a/web_src/js/svg.ts
+++ b/web_src/js/svg.ts
@@ -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';
@@ -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,
diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts
index bd872f094c..997a4d1ff3 100644
--- a/web_src/js/utils.ts
+++ b/web_src/js/utils.ts
@@ -134,16 +134,16 @@ export function toAbsoluteUrl(url: string): string {
   return `${window.location.origin}${url}`;
 }
 
-// Encode an ArrayBuffer into a URLEncoded base64 string.
-export function encodeURLEncodedBase64(arrayBuffer: ArrayBuffer): string {
-  return encode(arrayBuffer)
+// Encode an Uint8Array into a URLEncoded base64 string.
+export function encodeURLEncodedBase64(uint8Array: Uint8Array): string {
+  return encode(uint8Array)
     .replace(/\+/g, '-')
     .replace(/\//g, '_')
     .replace(/=/g, '');
 }
 
-// Decode a URLEncoded base64 to an ArrayBuffer.
-export function decodeURLEncodedBase64(base64url: string): ArrayBuffer {
+// Decode a URLEncoded base64 to an Uint8Array.
+export function decodeURLEncodedBase64(base64url: string): Uint8Array {
   return decode(base64url
     .replace(/_/g, '/')
     .replace(/-/g, '+'));