diff --git a/.deadcode-out b/.deadcode-out
new file mode 100644
index 0000000000..6b4114dd27
--- /dev/null
+++ b/.deadcode-out
@@ -0,0 +1,366 @@
+package "code.gitea.io/gitea/cmd"
+	func NoMainListener
+
+package "code.gitea.io/gitea/cmd/forgejo"
+	func ContextSetNoInit
+	func ContextSetNoExit
+	func ContextSetStderr
+	func ContextGetStderr
+	func ContextSetStdout
+	func ContextSetStdin
+
+package "code.gitea.io/gitea/models"
+	func IsErrUpdateTaskNotExist
+	func (ErrUpdateTaskNotExist).Error
+	func (ErrUpdateTaskNotExist).Unwrap
+	func IsErrSHANotFound
+	func GetYamlFixturesAccess
+
+package "code.gitea.io/gitea/models/actions"
+	func ListUploadedArtifactsByRunID
+	func CountRunJobs
+	func (ScheduleList).GetUserIDs
+	func (ScheduleList).GetRepoIDs
+	func (ScheduleList).LoadTriggerUser
+	func (ScheduleList).LoadRepos
+	func FindSchedules
+	func CountSpecs
+	func GetVariableByID
+
+package "code.gitea.io/gitea/models/asymkey"
+	func HasDeployKey
+
+package "code.gitea.io/gitea/models/auth"
+	func DeleteAuthTokenByID
+	func GetSourceByName
+	func GetWebAuthnCredentialByID
+	func WebAuthnCredentials
+
+package "code.gitea.io/gitea/models/db"
+	func TruncateBeans
+	func InTransaction
+	func DumpTables
+	func Count
+	func FindAndCount
+
+package "code.gitea.io/gitea/models/dbfs"
+	func (*file).renameTo
+	func Create
+	func Rename
+
+package "code.gitea.io/gitea/models/forgejo/semver"
+	func GetVersion
+	func SetVersionString
+	func SetVersion
+
+package "code.gitea.io/gitea/models/forgejo_migrations"
+	func GetCurrentDBVersion
+	func EnsureUpToDate
+
+package "code.gitea.io/gitea/models/git"
+	func RemoveDeletedBranchByID
+
+package "code.gitea.io/gitea/models/issues"
+	func IsErrUnknownDependencyType
+	func (ErrNewIssueInsert).Error
+	func IsErrIssueWasClosed
+	func GetIssueWithAttrsByID
+	func GetRepoIDsForIssuesOptions
+	func GetLabelIDsInOrgByNames
+	func ChangeMilestoneStatus
+	func GetMilestonesByRepoIDs
+	func CountMilestonesByRepoCond
+	func GetMilestonesStatsByRepoCond
+	func IsErrErrPullRequestHeadRepoMissing
+	func (ErrPullRequestHeadRepoMissing).Error
+	func GetPullRequestsByHeadBranch
+	func (ErrIssueStopwatchAlreadyExist).Error
+	func (ErrIssueStopwatchAlreadyExist).Unwrap
+
+package "code.gitea.io/gitea/models/migrations/base"
+	func removeAllWithRetry
+	func newXORMEngine
+	func deleteDB
+	func PrepareTestEnv
+	func MainTest
+
+package "code.gitea.io/gitea/models/organization"
+	func UpdateTeamUnits
+	func (SearchMembersOptions).ToConds
+	func UsersInTeamsCount
+
+package "code.gitea.io/gitea/models/packages/alpine"
+	func GetBranches
+	func GetRepositories
+	func GetArchitectures
+
+package "code.gitea.io/gitea/models/perm/access"
+	func GetRepoWriters
+
+package "code.gitea.io/gitea/models/project"
+	func UpdateBoardSorting
+	func ChangeProjectStatus
+
+package "code.gitea.io/gitea/models/repo"
+	func DeleteAttachmentsByIssue
+	func (*releaseSorter).Len
+	func (*releaseSorter).Less
+	func (*releaseSorter).Swap
+	func SortReleases
+	func (SearchOrderBy).String
+	func IsErrTopicNotExist
+	func (ErrTopicNotExist).Error
+	func (ErrTopicNotExist).Unwrap
+	func GetTopicByName
+	func WatchRepoMode
+
+package "code.gitea.io/gitea/models/system"
+	func DeleteNotice
+
+package "code.gitea.io/gitea/models/unittest"
+	func CheckConsistencyFor
+	func checkForConsistency
+	func GetXORMEngine
+	func OverrideFixtures
+	func InitFixtures
+	func LoadFixtures
+	func Copy
+	func CopyDir
+	func FixturesDir
+	func fatalTestError
+	func InitSettings
+	func MainTest
+	func CreateTestEngine
+	func PrepareTestDatabase
+	func PrepareTestEnv
+	func Cond
+	func OrderBy
+	func LoadBeanIfExists
+	func BeanExists
+	func AssertExistsAndLoadBean
+	func GetCount
+	func AssertNotExistsBean
+	func AssertExistsIf
+	func AssertSuccessfulInsert
+	func AssertCount
+	func AssertInt64InRange
+
+package "code.gitea.io/gitea/models/user"
+	func IsErrPrimaryEmailCannotDelete
+	func (ErrUserInactive).Error
+	func (ErrUserInactive).Unwrap
+	func IsErrExternalLoginUserAlreadyExist
+	func IsErrExternalLoginUserNotExist
+	func IsErrUserSettingIsNotExist
+	func GetUserAllSettings
+	func DeleteUserSetting
+	func GetUserEmailsByNames
+
+package "code.gitea.io/gitea/modules/activitypub"
+	func CurrentTime
+	func containsRequiredHTTPHeaders
+	func NewClient
+	func (*Client).NewRequest
+	func (*Client).Post
+	func GetPrivateKey
+
+package "code.gitea.io/gitea/modules/assetfs"
+	func Bindata
+
+package "code.gitea.io/gitea/modules/auth/password/hash"
+	func (*DummyHasher).HashWithSaltBytes
+	func NewDummyHasher
+
+package "code.gitea.io/gitea/modules/auth/password/pwn"
+	func WithHTTP
+
+package "code.gitea.io/gitea/modules/base"
+	func BasicAuthEncode
+	func IsLetter
+	func SetupGiteaRoot
+
+package "code.gitea.io/gitea/modules/cache"
+	func GetInt
+	func WithNoCacheContext
+	func RemoveContextData
+
+package "code.gitea.io/gitea/modules/charset"
+	func (*BreakWriter).Write
+	func ToUTF8
+	func EscapeControlString
+
+package "code.gitea.io/gitea/modules/context"
+	func GetPrivateContext
+
+package "code.gitea.io/gitea/modules/emoji"
+	func ReplaceCodes
+
+package "code.gitea.io/gitea/modules/eventsource"
+	func (*Event).String
+
+package "code.gitea.io/gitea/modules/git"
+	func AllowLFSFiltersArgs
+	func AddChanges
+	func AddChangesWithArgs
+	func CommitChanges
+	func CommitChangesWithArgs
+	func IsErrExecTimeout
+	func (ErrExecTimeout).Error
+	func (ErrUnsupportedVersion).Error
+	func SetUpdateHook
+	func openRepositoryWithDefaultContext
+	func GetBranchCommitID
+	func IsTagExist
+	func ToEntryMode
+	func (*LimitedReaderCloser).Read
+	func (*LimitedReaderCloser).Close
+
+package "code.gitea.io/gitea/modules/gitgraph"
+	func (*Parser).Reset
+
+package "code.gitea.io/gitea/modules/graceful"
+	func (*Manager).TerminateContext
+	func (*Manager).IsTerminate
+	func (*Manager).Err
+	func (*Manager).Value
+	func (*Manager).Deadline
+
+package "code.gitea.io/gitea/modules/hcaptcha"
+	func WithHTTP
+
+package "code.gitea.io/gitea/modules/json"
+	func (StdJSON).Marshal
+	func (StdJSON).Unmarshal
+	func (StdJSON).NewEncoder
+	func (StdJSON).NewDecoder
+	func (StdJSON).Indent
+
+package "code.gitea.io/gitea/modules/markup"
+	func GetRendererByType
+	func RenderString
+	func IsMarkupFile
+
+package "code.gitea.io/gitea/modules/markup/console"
+	func Render
+	func RenderString
+
+package "code.gitea.io/gitea/modules/markup/markdown"
+	func IsDetails
+	func IsSummary
+	func IsTaskCheckBoxListItem
+	func IsIcon
+	func IsColorPreview
+	func RenderRawString
+
+package "code.gitea.io/gitea/modules/markup/markdown/math"
+	func WithInlineDollarParser
+	func WithBlockDollarParser
+
+package "code.gitea.io/gitea/modules/markup/mdstripper"
+	func StripMarkdown
+
+package "code.gitea.io/gitea/modules/markup/orgmode"
+	func RenderString
+
+package "code.gitea.io/gitea/modules/private"
+	func ActionsRunnerRegister
+
+package "code.gitea.io/gitea/modules/process"
+	func (*Manager).ExecTimeout
+
+package "code.gitea.io/gitea/modules/queue"
+	func newBaseChannelSimple
+	func newBaseChannelUnique
+	func newBaseRedisSimple
+	func newBaseRedisUnique
+	func newWorkerPoolQueueForTest
+
+package "code.gitea.io/gitea/modules/queue/lqinternal"
+	func QueueItemIDBytes
+	func QueueItemKeyBytes
+	func ListLevelQueueKeys
+
+package "code.gitea.io/gitea/modules/setting"
+	func NewConfigProviderFromData
+	func (*GitConfigType).GetOption
+	func InitLoggersForTest
+
+package "code.gitea.io/gitea/modules/storage"
+	func (ErrInvalidConfiguration).Error
+	func IsErrInvalidConfiguration
+
+package "code.gitea.io/gitea/modules/structs"
+	func ParseCreateHook
+	func ParsePushHook
+
+package "code.gitea.io/gitea/modules/sync"
+	func (*StatusTable).Start
+	func (*StatusTable).IsRunning
+
+package "code.gitea.io/gitea/modules/testlogger"
+	func (*testLoggerWriterCloser).pushT
+	func (*testLoggerWriterCloser).Write
+	func (*testLoggerWriterCloser).popT
+	func (*testLoggerWriterCloser).Close
+	func (*testLoggerWriterCloser).Reset
+	func PrintCurrentTest
+	func Printf
+	func NewTestLoggerWriter
+
+package "code.gitea.io/gitea/modules/timeutil"
+	func GetExecutableModTime
+	func Set
+	func Unset
+
+package "code.gitea.io/gitea/modules/translation"
+	func (MockLocale).Language
+	func (MockLocale).Tr
+	func (MockLocale).TrN
+	func (MockLocale).PrettyNumber
+
+package "code.gitea.io/gitea/modules/util/filebuffer"
+	func CreateFromReader
+
+package "code.gitea.io/gitea/modules/web"
+	func RouteMock
+	func RouteMockReset
+
+package "code.gitea.io/gitea/modules/web/middleware"
+	func DeleteLocaleCookie
+
+package "code.gitea.io/gitea/routers/web"
+	func NotFound
+
+package "code.gitea.io/gitea/routers/web/org"
+	func MustEnableProjects
+	func getActionIssues
+	func UpdateIssueProject
+
+package "code.gitea.io/gitea/services/convert"
+	func ToSecret
+
+package "code.gitea.io/gitea/services/forms"
+	func (*DeadlineForm).Validate
+
+package "code.gitea.io/gitea/services/packages/alpine"
+	func BuildAllRepositoryFiles
+
+package "code.gitea.io/gitea/services/pull"
+	func IsCommitStatusContextSuccess
+
+package "code.gitea.io/gitea/services/repository"
+	func GetBranchCommitID
+	func IsErrForkAlreadyExist
+
+package "code.gitea.io/gitea/services/repository/archiver"
+	func ArchiveRepository
+
+package "code.gitea.io/gitea/services/repository/files"
+	func (*ContentType).String
+	func GetFileResponseFromCommit
+	func (*TemporaryUploadRepository).GetLastCommit
+	func (*TemporaryUploadRepository).GetLastCommitByRef
+
+package "code.gitea.io/gitea/services/webhook"
+	func NewNotifier
+
diff --git a/.gitignore b/.gitignore
index f89f5021b1..bc81ad4324 100644
--- a/.gitignore
+++ b/.gitignore
@@ -96,6 +96,7 @@ cpu.out
 /VERSION
 /.air
 /.go-licenses
+/.cur-deadcode-out
 
 # Snapcraft
 /gitea_a*.txt
diff --git a/Makefile b/Makefile
index 280b340a30..8944242800 100644
--- a/Makefile
+++ b/Makefile
@@ -22,6 +22,7 @@ GO ?= go
 SHASUM ?= shasum -a 256
 HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
 COMMA := ,
+DIFF ?= diff --unified
 
 XGO_VERSION := go-1.21.x
 
@@ -36,6 +37,7 @@ XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
 GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0
 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.0.1
 ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.6.25
+DEADCODE_PACKAGE ?= golang.org/x/tools/internal/cmd/deadcode@v0.14.0
 
 DOCKER_IMAGE ?= gitea/gitea
 DOCKER_TAG ?= latest
@@ -409,10 +411,17 @@ lint-md: node_modules
 .PHONY: lint-go
 lint-go:
 	$(GO) run $(GOLANGCI_LINT_PACKAGE) run $(GOLANGCI_LINT_ARGS)
+	$(GO) run $(DEADCODE_PACKAGE) -generated=false -test code.gitea.io/gitea > .cur-deadcode-out
+	@$(DIFF) .deadcode-out .cur-deadcode-out; \
+	if [ $$? -eq 1 ]; then \
+		echo "Please run 'make lint-go-fix' and commit the result"; \
+		exit 1; \
+	fi
 
 .PHONY: lint-go-fix
 lint-go-fix:
 	$(GO) run $(GOLANGCI_LINT_PACKAGE) run $(GOLANGCI_LINT_ARGS) --fix
+	$(GO) run $(DEADCODE_PACKAGE) -generated=false -test code.gitea.io/gitea > .deadcode-out
 
 # workaround step for the lint-go-windows CI task because 'go run' can not
 # have distinct GOOS/GOARCH for its build and run steps
diff --git a/modules/util/legacy.go b/modules/util/legacy.go
index 2ea293a2be..2d4de01949 100644
--- a/modules/util/legacy.go
+++ b/modules/util/legacy.go
@@ -4,10 +4,6 @@
 package util
 
 import (
-	"crypto/aes"
-	"crypto/cipher"
-	"crypto/rand"
-	"errors"
 	"io"
 	"os"
 )
@@ -40,52 +36,3 @@ func CopyFile(src, dest string) error {
 	}
 	return os.Chmod(dest, si.Mode())
 }
-
-// AESGCMEncrypt (from legacy package): encrypts plaintext with the given key using AES in GCM mode. should be replaced.
-func AESGCMEncrypt(key, plaintext []byte) ([]byte, error) {
-	block, err := aes.NewCipher(key)
-	if err != nil {
-		return nil, err
-	}
-
-	gcm, err := cipher.NewGCM(block)
-	if err != nil {
-		return nil, err
-	}
-
-	nonce := make([]byte, gcm.NonceSize())
-	if _, err := rand.Read(nonce); err != nil {
-		return nil, err
-	}
-
-	ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
-	return append(nonce, ciphertext...), nil
-}
-
-// AESGCMDecrypt (from legacy package): decrypts ciphertext with the given key using AES in GCM mode. should be replaced.
-func AESGCMDecrypt(key, ciphertext []byte) ([]byte, error) {
-	block, err := aes.NewCipher(key)
-	if err != nil {
-		return nil, err
-	}
-
-	gcm, err := cipher.NewGCM(block)
-	if err != nil {
-		return nil, err
-	}
-
-	size := gcm.NonceSize()
-	if len(ciphertext)-size <= 0 {
-		return nil, errors.New("ciphertext is empty")
-	}
-
-	nonce := ciphertext[:size]
-	ciphertext = ciphertext[size:]
-
-	plainText, err := gcm.Open(nil, nonce, ciphertext, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	return plainText, nil
-}
diff --git a/modules/util/legacy_test.go b/modules/util/legacy_test.go
index e732094c29..b7991bd365 100644
--- a/modules/util/legacy_test.go
+++ b/modules/util/legacy_test.go
@@ -4,8 +4,6 @@
 package util
 
 import (
-	"crypto/aes"
-	"crypto/rand"
 	"fmt"
 	"os"
 	"testing"
@@ -37,21 +35,3 @@ func TestCopyFile(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, testContent, dstContent)
 }
-
-func TestAESGCM(t *testing.T) {
-	t.Parallel()
-
-	key := make([]byte, aes.BlockSize)
-	_, err := rand.Read(key)
-	assert.NoError(t, err)
-
-	plaintext := []byte("this will be encrypted")
-
-	ciphertext, err := AESGCMEncrypt(key, plaintext)
-	assert.NoError(t, err)
-
-	decrypted, err := AESGCMDecrypt(key, ciphertext)
-	assert.NoError(t, err)
-
-	assert.Equal(t, plaintext, decrypted)
-}