Upgrade go to 1.25.9 and golangci-lint (#6869)

* Bump go version in go.mod
* Update compiler image.

Changed github download url since existing one didn't have version 12 of the SDK.

* Update macOS requirements in README for v0.32.0
* Update lint action
* Bump golangci-lint version
* Migrate golangci-lint config
* Fix QF1012 errors

(Use fmt.Fprintf(...) instead of WriteString(fmt.Sprintf(...)))

* Fix QF1003 errors

(could use tagged switch)

* Fix ST1005 errors

(error string capitalisation)

* Fix ST1011 errors

(seconds suffix)

* Fix QF1006 errors

(lift into loop condition)

* Fix QF1002 errors

(switch condition)

* Fix gocritic error

(deprecated paragraph)

* Fix incorrect nolint directive

* Ignore specific checks

noctx should be addressed in a later PR
---------
Co-authored-by: DogmaDragon <103123951+DogmaDragon@users.noreply.github.com>
Co-authored-by: feederbox826 <me@feederbox.cc>
This commit is contained in:
WithoutPants 2026-04-29 10:13:58 +10:00 committed by GitHub
parent 98fd0267d0
commit 2b29207f1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 156 additions and 128 deletions

View file

@ -4,7 +4,7 @@ on:
workflow_dispatch:
env:
COMPILER_IMAGE: ghcr.io/stashapp/compiler:13
COMPILER_IMAGE: ghcr.io/stashapp/compiler:14
jobs:
build-compiler:

View file

@ -15,7 +15,7 @@ concurrency:
cancel-in-progress: true
env:
COMPILER_IMAGE: ghcr.io/stashapp/compiler:13
COMPILER_IMAGE: ghcr.io/stashapp/compiler:14
jobs:
# Job 1: Generate code and build UI
@ -30,6 +30,8 @@ jobs:
fetch-tags: true
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
# pnpm version is read from the packageManager field in package.json
# very broken (4.3, 4.4)

View file

@ -17,6 +17,8 @@ jobs:
# no tags or depth needed for lint
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
# generate-backend runs natively (just go generate + touch-ui) — no Docker needed
- name: Generate Backend
@ -25,4 +27,6 @@ jobs:
## WARN
## using v1, update in a later PR
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v8
with:
version: v2.11.4

View file

@ -1,87 +1,100 @@
# options for analysis running
run:
timeout: 5m
version: "2"
linters:
disable-all: true
default: none
enable:
# Default set of linters from golangci-lint
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- typecheck
- unused
# Linters added by the stash project.
# - contextcheck
- copyloopvar
- dogsled
- errcheck
- errchkjson
- errorlint
# - exhaustive
- gocritic
# - goerr113
- gofmt
# - gomnd
# - ifshort
- govet
- ineffassign
- misspell
# - nakedret
- noctx
# TODO - fix these in a later PR
# - noctx
- revive
- rowserrcheck
- sqlclosecheck
# Project-specific linter overrides
linters-settings:
gofmt:
simplify: false
errorlint:
# Disable errorf because there are false positives, where you don't want to wrap
# an error.
errorf: false
asserts: true
comparison: true
revive:
ignore-generated-header: true
severity: error
confidence: 0.8
rules:
- name: blank-imports
disabled: true
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
disabled: true
- name: if-return
disabled: true
- name: increment-decrement
- name: var-naming
disabled: true
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
disabled: true
- name: indent-error-flow
disabled: true
- name: errorf
- name: empty-block
disabled: true
- name: superfluous-else
- name: unused-parameter
disabled: true
- name: unreachable-code
- name: redefines-builtin-id
rowserrcheck:
packages:
- github.com/jmoiron/sqlx
- staticcheck
- unused
settings:
staticcheck:
checks:
- all
# we specify (unnecessary) embedded fields for clarity in many places
- -QF1008
# there's lots of misnamed (eg intId instead of intID) fields in the code.
# it's not exactly world-ending, so I'm deferring fixing these for now
- -ST1003
errorlint:
errorf: false
asserts: true
comparison: true
revive:
confidence: 0.8
severity: error
rules:
- name: blank-imports
disabled: true
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
disabled: true
- name: if-return
disabled: true
- name: increment-decrement
- name: var-naming
disabled: true
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
disabled: true
- name: indent-error-flow
disabled: true
- name: errorf
- name: empty-block
disabled: true
- name: superfluous-else
- name: unused-parameter
disabled: true
- name: unreachable-code
- name: redefines-builtin-id
rowserrcheck:
packages:
- github.com/jmoiron/sqlx
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
settings:
gofmt:
simplify: false
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View file

@ -36,7 +36,8 @@ Step-by-step instructions are available at [docs.stashapp.cc/installation](https
> **macOS Users**
>
> As of version 0.29.0, Stash requires _macOS 11 Big Sur_ or later.
> Stash can still be run through docker on older versions of macOS.
> As of version 0.32.0, Stash requires _macOS 12 Monterey_ or later.
> Stash can still be run through Docker on older versions of macOS.
<img src="docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS | <img src="docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="docs/readme_assets/docker_logo.svg" width="100%" height="75"> Docker
:---:|:---:|:---:|:---:

View file

@ -148,7 +148,7 @@ func recoverPanic() {
exitCode = 1
logger.Errorf("panic: %v\n%s", err, debug.Stack())
if desktop.IsDesktop() {
desktop.FatalError(fmt.Errorf("Panic: %v", err))
desktop.FatalError(fmt.Errorf("panic: %v", err))
}
}
}

View file

@ -5,14 +5,14 @@ WORKDIR /tmp/osxcross
ARG OSXCROSS_REVISION=5e1b71fcceb23952f3229995edca1b6231525b5b
ADD --checksum=sha256:d3f771bbc20612fea577b18a71be3af2eb5ad2dd44624196cf55de866d008647 https://codeload.github.com/tpoechtrager/osxcross/tar.gz/${OSXCROSS_REVISION} /tmp/osxcross.tar.gz
ARG OSX_SDK_VERSION=11.3
ARG OSX_SDK_VERSION=12.3
ARG OSX_SDK_DOWNLOAD_FILE=MacOSX${OSX_SDK_VERSION}.sdk.tar.xz
ARG OSX_SDK_DOWNLOAD_URL=https://github.com/phracker/MacOSX-SDKs/releases/download/${OSX_SDK_VERSION}/${OSX_SDK_DOWNLOAD_FILE}
ADD --checksum=sha256:cd4f08a75577145b8f05245a2975f7c81401d75e9535dcffbb879ee1deefcbf4 ${OSX_SDK_DOWNLOAD_URL} /tmp/osxcross/tarballs/${OSX_SDK_DOWNLOAD_FILE}
ARG OSX_SDK_DOWNLOAD_URL=https://github.com/joseluisq/macosx-sdks/releases/download/${OSX_SDK_VERSION}/${OSX_SDK_DOWNLOAD_FILE}
ADD --checksum=sha256:3abd261ceb483c44295a6623fdffe5d44fc4ac2c872526576ec5ab5ad0f6e26c ${OSX_SDK_DOWNLOAD_URL} /tmp/osxcross/tarballs/${OSX_SDK_DOWNLOAD_FILE}
ENV UNATTENDED=yes \
SDK_VERSION=${OSX_SDK_VERSION} \
OSX_VERSION_MIN=10.10
OSX_VERSION_MIN=12.0
RUN apt update && \
apt install -y --no-install-recommends \
bash ca-certificates clang cmake git patch libssl-dev bzip2 cpio libbz2-dev libxml2-dev make python3 xz-utils zlib1g-dev
@ -46,7 +46,7 @@ RUN cd /opt/cross-freebsd/usr/lib && \
ln -s libc++.so libstdc++.so
### BUILDER
FROM golang:1.24.3 AS builder
FROM golang:1.25.9 AS builder
ENV PATH=/opt/osx-ndk-x86/bin:$PATH
# copy in nodejs instead of using nodesource :thumbsup:

View file

@ -1,7 +1,7 @@
host=ghcr.io
user=stashapp
repo=compiler
version=13
version=14
VERSION_IMAGE = ${host}/${user}/${repo}:${version}
LATEST_IMAGE = ${host}/${user}/${repo}:latest

2
go.mod
View file

@ -1,6 +1,6 @@
module github.com/stashapp/stash
go 1.24.3
go 1.25
require (
github.com/99designs/gqlgen v0.17.73

View file

@ -148,12 +148,12 @@ func makeGithubRequest(ctx context.Context, url string, output interface{}) erro
response, err := client.Do(req)
if err != nil {
//lint:ignore ST1005 Github is a proper capitalized noun
//nolint:staticcheck // ST1005 Github is a proper capitalized noun
return fmt.Errorf("Github API request failed: %w", err)
}
if response.StatusCode != http.StatusOK {
//lint:ignore ST1005 Github is a proper capitalized noun
//nolint:staticcheck // ST1005 Github is a proper capitalized noun
return fmt.Errorf("Github API request failed: %s", response.Status)
}
@ -161,7 +161,7 @@ func makeGithubRequest(ctx context.Context, url string, output interface{}) erro
data, err := io.ReadAll(response.Body)
if err != nil {
//lint:ignore ST1005 Github is a proper capitalized noun
//nolint:staticcheck // ST1005 Github is a proper capitalized noun
return fmt.Errorf("Github API read response failed: %w", err)
}
@ -295,10 +295,10 @@ func printLatestVersion(ctx context.Context) {
logger.Errorf("Couldn't retrieve latest version: %v", err)
} else {
_, githash, _ := build.Version()
switch {
case githash == "":
switch githash {
case "":
logger.Infof("Latest version: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
case githash == latestRelease.ShortHash:
case latestRelease.ShortHash:
logger.Infof("Version %s (%s) is already the latest released", latestRelease.Version, latestRelease.ShortHash)
default:
logger.Infof("New version available: %s (%s)", latestRelease.Version, latestRelease.ShortHash)

View file

@ -12,9 +12,10 @@ import (
func refreshPackageType(typeArg PackageType) {
mgr := manager.GetInstance()
if typeArg == PackageTypePlugin {
switch typeArg {
case PackageTypePlugin:
mgr.RefreshPluginCache()
} else if typeArg == PackageTypeScraper {
case PackageTypeScraper:
mgr.RefreshScraperCache()
}
}

View file

@ -654,7 +654,7 @@ func (r *mutationResolver) PerformerMerge(ctx context.Context, input PerformerMe
}
legacyURLs := legacyPerformerURLsFromInput(*input.Values, translator)
if legacyURLs.AnySet() {
return nil, errors.New("Merging legacy performer URLs is not supported")
return nil, errors.New("merging legacy performer URLs is not supported")
}
if input.Values.Image != nil {

View file

@ -33,8 +33,10 @@ type MetadataOptions struct {
SetCoverImage *bool `json:"setCoverImage"`
SetOrganized *bool `json:"setOrganized"`
// defaults to true if not provided
// Deprecated: use PerformerGenders instead
IncludeMalePerformers *bool `json:"includeMalePerformers"`
// Filter to only include performers with these genders. If not provided, all genders are included.
PerformerGenders []models.GenderEnum `json:"performerGenders"`
// defaults to true if not provided

View file

@ -22,7 +22,8 @@ type SceneMissingHashCounter interface {
// will ensure that all oshash values are set on all scenes.
func ValidateVideoFileNamingAlgorithm(ctx context.Context, qb SceneMissingHashCounter, newValue models.HashAlgorithm) error {
// if algorithm is being set to MD5, then all checksums must be present
if newValue == models.HashAlgorithmMd5 {
switch newValue {
case models.HashAlgorithmMd5:
missingMD5, err := qb.CountMissingChecksum(ctx)
if err != nil {
return err
@ -31,7 +32,7 @@ func ValidateVideoFileNamingAlgorithm(ctx context.Context, qb SceneMissingHashCo
if missingMD5 > 0 {
return errors.New("some checksums are missing on scenes. Run Scan with calculateMD5 set to true")
}
} else if newValue == models.HashAlgorithmOshash {
case models.HashAlgorithmOshash:
missingOSHash, err := qb.CountMissingOSHash(ctx)
if err != nil {
return err

View file

@ -408,7 +408,7 @@ func ConvertFunscriptToCSV(funscriptPath string) ([]byte, error) {
}
// I don't know whether the csv format requires int or float, so for now we'll use int
buffer.WriteString(fmt.Sprintf("%d,%d\r\n", int(math.Round(action.At)), pos))
fmt.Fprintf(&buffer, "%d,%d\r\n", int(math.Round(action.At)), pos)
}
return buffer.Bytes(), nil
}

View file

@ -76,9 +76,10 @@ func performImport(ctx context.Context, i importer, duplicateBehaviour ImportDup
var id int
if existing != nil {
if duplicateBehaviour == ImportDuplicateEnumFail {
switch duplicateBehaviour {
case ImportDuplicateEnumFail:
return fmt.Errorf("existing object with name '%s'", name)
} else if duplicateBehaviour == ImportDuplicateEnumIgnore {
case ImportDuplicateEnumIgnore:
logger.Infof("Skipping existing object %q", name)
return nil
}

View file

@ -35,7 +35,7 @@ func (j *OptimiseDatabaseJob) Execute(ctx context.Context, progress *job.Progres
return nil
}
if err != nil {
return fmt.Errorf("Error analyzing database: %w", err)
return fmt.Errorf("error analyzing database: %w", err)
}
progress.ExecuteTask("Vacuuming database", func() {

View file

@ -20,12 +20,12 @@ func (s *Manager) RunPluginTask(
pluginProgress := make(chan float64)
task, err := s.PluginCache.CreateTask(ctx, pluginID, taskName, args, pluginProgress)
if err != nil {
return fmt.Errorf("Error creating plugin task: %w", err)
return fmt.Errorf("error creating plugin task: %w", err)
}
err = task.Start()
if err != nil {
return fmt.Errorf("Error running plugin task: %w", err)
return fmt.Errorf("error running plugin task: %w", err)
}
done := make(chan bool)

View file

@ -45,13 +45,13 @@ func (f *FFMpeg) InitHWSupport(ctx context.Context) {
// log if the initialization takes too long
const hwInitLogTimeoutSecondsDefault = 5
hwInitLogTimeoutSeconds := hwInitLogTimeoutSecondsDefault * time.Second
timer := time.NewTimer(hwInitLogTimeoutSeconds)
hwInitLogTimeout := hwInitLogTimeoutSecondsDefault * time.Second
timer := time.NewTimer(hwInitLogTimeout)
go func() {
select {
case <-timer.C:
logger.Warnf("[InitHWSupport] Hardware codec initialization is taking longer than %s...", hwInitLogTimeoutSeconds)
logger.Warnf("[InitHWSupport] Hardware codec initialization is taking longer than %s...", hwInitLogTimeout)
logger.Info("[InitHWSupport] Hardware encoding will not be available until initialization is complete.")
case <-done:
if !timer.Stop() {
@ -96,16 +96,16 @@ func (f *FFMpeg) initHWSupport(ctx context.Context) {
// #6064 - add timeout to context to prevent hangs
const hwTestTimeoutSecondsDefault = 10
hwTestTimeoutSeconds := hwTestTimeoutSecondsDefault * time.Second
hwTestTimeout := hwTestTimeoutSecondsDefault * time.Second
// allow timeout to be overridden with environment variable
if timeout := os.Getenv("STASH_HW_TEST_TIMEOUT"); timeout != "" {
if seconds, err := strconv.Atoi(timeout); err == nil {
hwTestTimeoutSeconds = time.Duration(seconds) * time.Second
hwTestTimeout = time.Duration(seconds) * time.Second
}
}
testCtx, cancel := context.WithTimeout(ctx, hwTestTimeoutSeconds)
testCtx, cancel := context.WithTimeout(ctx, hwTestTimeout)
defer cancel()
cmd := f.Command(testCtx, args)
@ -117,7 +117,7 @@ func (f *FFMpeg) initHWSupport(ctx context.Context) {
if err := cmd.Run(); err != nil {
if testCtx.Err() != nil {
logger.Debugf("[InitHWSupport] Codec %s test timed out after %s", codec, hwTestTimeoutSeconds)
logger.Debugf("[InitHWSupport] Codec %s test timed out after %s", codec, hwTestTimeout)
continue
}

View file

@ -142,6 +142,8 @@ func (f *StashIgnoreFilter) collectIgnoreEntries(dir string, libraryRoot string)
current := dir
for {
// Check if we're still within the library root.
// nolint:staticcheck // QF1006 - we could make this the for condition
// but I don't think it improves readability
if !isPathInOrEqual(libraryRoot, current) {
break
}

View file

@ -69,19 +69,19 @@ type ScanHandler struct {
func (h *ScanHandler) validate() error {
if h.CreatorUpdater == nil {
return errors.New("CreatorUpdater is required")
return errors.New("internal error: CreatorUpdater is required")
}
if h.ScanGenerator == nil {
return errors.New("ScanGenerator is required")
return errors.New("internal error: ScanGenerator is required")
}
if h.GalleryFinder == nil {
return errors.New("GalleryFinder is required")
return errors.New("internal error: GalleryFinder is required")
}
if h.ScanConfig == nil {
return errors.New("ScanConfig is required")
return errors.New("internal error: ScanConfig is required")
}
if h.Paths == nil {
return errors.New("Paths is required")
return errors.New("internal error: Paths is required")
}
return nil
@ -375,13 +375,13 @@ func (h *ScanHandler) getOrCreateGallery(ctx context.Context, f models.File) (*m
if _, err := os.Stat(filepath.Join(folderPath, ".forcegallery")); err == nil {
forceGallery = true
} else if !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("Could not test Path %s: %w", folderPath, err)
return nil, fmt.Errorf("could not test Path %s: %w", folderPath, err)
}
exemptGallery := false
if _, err := os.Stat(filepath.Join(folderPath, ".nogallery")); err == nil {
exemptGallery = true
} else if !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("Could not test Path %s: %w", folderPath, err)
return nil, fmt.Errorf("could not test Path %s: %w", folderPath, err)
}
if forceGallery || (h.ScanConfig.GetCreateGalleriesFromFolders() && !exemptGallery) {

View file

@ -97,9 +97,10 @@ func (j *Job) TimeElapsed() time.Duration {
}
func (j *Job) cancel() {
if j.Status == StatusReady {
switch j.Status {
case StatusReady:
j.Status = StatusCancelled
} else if j.Status == StatusRunning {
case StatusRunning:
j.Status = StatusStopping
}

View file

@ -232,7 +232,7 @@ func (g Generator) generateConcatFile(chunkFiles []string) (fn string, err error
for _, f := range chunkFiles {
// files in concat file should be relative to concat
relFile := filepath.Base(f)
if _, err := w.WriteString(fmt.Sprintf("file '%s'\n", relFile)); err != nil {
if _, err := fmt.Fprintf(w, "file '%s'\n", relFile); err != nil {
return concatFile.Name(), fmt.Errorf("writing concat file: %w", err)
}
}

View file

@ -57,19 +57,19 @@ type ScanHandler struct {
func (h *ScanHandler) validate() error {
if h.CreatorUpdater == nil {
return errors.New("CreatorUpdater is required")
return errors.New("internal error: CreatorUpdater is required")
}
if h.ScanGenerator == nil {
return errors.New("ScanGenerator is required")
return errors.New("internal error: ScanGenerator is required")
}
if h.CaptionUpdater == nil {
return errors.New("CaptionUpdater is required")
return errors.New("internal error: CaptionUpdater is required")
}
if !h.FileNamingAlgorithm.IsValid() {
return errors.New("FileNamingAlgorithm is required")
return errors.New("internal error: FileNamingAlgorithm is required")
}
if h.Paths == nil {
return errors.New("Paths is required")
return errors.New("internal error: Paths is required")
}
return nil

View file

@ -93,7 +93,7 @@ func (r *updateRecord) setTimestamp(destField string, v models.OptionalTime) {
}
}
//nolint:golint,unused
//nolint:unused
func (r *updateRecord) setNullTimestamp(destField string, v models.OptionalTime) {
if v.Set {
r.set(destField, NullTimestampFromTimePtr(v.Ptr()))