Update golangci-lint to v2.12.2 with canonical config (#187)
All checks were successful
Check / check (push) Successful in 4s

Bumps golangci-lint from v2.10.1 to v2.12.2 everywhere it is pinned and installs the canonical `.golangci.yml`, then fixes every finding the new linter surfaces so `make check` is green.

## Version pins

- `Dockerfile` lint stage: `golangci/golangci-lint:v2.12.2` (Debian-based), tag plus digest pin
- `script/bootstrap`: `GOLANGCI_LINT_VERSION=2.12.2` with updated `linux-amd64`/`linux-arm64` release-archive sha256 pins

## Config

`.golangci.yml` replaced with the canonical config. Material change: the old file declared `version: "2"` but kept settings under the legacy top-level `linters-settings` key, which golangci-lint v2 ignores — so the intended thresholds (`lll` 88, `funlen` 80/50, `cyclop` 15, `dupl` 100) were not being applied. The canonical file moves them under `linters.settings` and drops `issues.exclude-use-default`.

## Lint fixes (216 findings)

- `lll` (96): wrapped lines to the 88-column limit
- `noctx` (46): `httptest.NewRequestWithContext` with `t.Context()` throughout the tests
- `goconst` (24): shared constants for template/JSON keys in `internal/handlers` and repeated test literals
- `gosec` (23): app-page redirects now go through a `redirectToApp` helper that path-escapes the app ID (G710 open redirect); `http.ServeFile` of the internally derived deployment log path annotated like the adjacent `os.Stat` (G703)
- `dupl` (22): extracted a generic `findAllByAppID` in `internal/models`, a `deleteAppResource` helper in `internal/handlers`, a shared `parsePush` in `internal/service/webhook`, and table-driven/helper-based dedup in tests
- `nolintlint` (5): removed `//nolint:funlen` directives made obsolete by the new limits (plus one more that became obsolete after refactoring)
- `nilerr` (3, surfaced during fixing): resource-delete lookups now propagate the find error to the caller

No behavior changes intended; all tests pass and `make check` is green.

Note: golangci-lint v2.12 warns that `gomodguard` is deprecated in favor of `gomodguard_v2` — a future canonical-config update should address this centrally.
Co-authored-by: sneak <sneak@sneak.berlin>
Reviewed-on: #187
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
This commit was merged in pull request #187.
This commit is contained in:
2026-08-07 22:21:42 +02:00
committed by Jeffrey Paul
parent 291f85f3ed
commit 7a34fc999c
41 changed files with 1172 additions and 797 deletions

View File

@@ -66,7 +66,8 @@ const logFilePermissions = 0o640
// logTimestampFormat is the format for log file timestamps.
const logTimestampFormat = "20060102T150405Z"
// logFileShortSHALength is the number of characters to use for commit SHA in log filenames.
// logFileShortSHALength is the number of characters to use for commit SHA
// in log filenames.
const logFileShortSHALength = 12
// dockerLogMessage represents a Docker build log message.
@@ -87,7 +88,10 @@ type deploymentLogWriter struct {
flushCtx context.Context //nolint:containedctx // needed for async flush goroutine
}
func newDeploymentLogWriter(ctx context.Context, deployment *models.Deployment) *deploymentLogWriter {
func newDeploymentLogWriter(
ctx context.Context,
deployment *models.Deployment,
) *deploymentLogWriter {
w := &deploymentLogWriter{
deployment: deployment,
done: make(chan struct{}),
@@ -257,7 +261,10 @@ func (svc *Service) GetBuildDir(appName string) string {
// GetLogFilePath returns the path to the log file for a deployment.
// Returns empty string if the path cannot be determined.
func (svc *Service) GetLogFilePath(app *models.App, deployment *models.Deployment) string {
func (svc *Service) GetLogFilePath(
app *models.App,
deployment *models.Deployment,
) string {
hostname, err := os.Hostname()
if err != nil {
hostname = "unknown"
@@ -275,7 +282,8 @@ func (svc *Service) GetLogFilePath(app *models.App, deployment *models.Deploymen
// Use started_at timestamp
timestamp := deployment.StartedAt.UTC().Format(logTimestampFormat)
// Build filename: appname_sha_timestamp.log.txt (or appname_timestamp.log.txt if no SHA)
// Build filename: appname_sha_timestamp.log.txt
// (or appname_timestamp.log.txt if no SHA)
var filename string
if sha != "" {
filename = fmt.Sprintf("%s_%s_%s.log.txt", app.Name, sha, timestamp)
@@ -308,7 +316,8 @@ func (svc *Service) CancelDeploy(appID string) bool {
// Deploy deploys an app. If cancelExisting is true (e.g. webhook-triggered),
// any in-progress deploy for the same app will be cancelled before starting.
// If cancelExisting is false and a deploy is in progress, ErrDeploymentInProgress is returned.
// If cancelExisting is false and a deploy is in progress,
// ErrDeploymentInProgress is returned.
func (svc *Service) Deploy(
ctx context.Context,
app *models.App,
@@ -342,7 +351,8 @@ func (svc *Service) Deploy(
// Fetch webhook event and create deployment record
webhookEvent := svc.fetchWebhookEvent(deployCtx, webhookEventID)
// Use a background context for DB operations that must complete regardless of cancellation
// Use a background context for DB operations that must complete
// regardless of cancellation
bgCtx := context.WithoutCancel(deployCtx)
deployment, err := svc.createDeploymentRecord(bgCtx, app, webhookEventID, webhookEvent)
@@ -401,7 +411,10 @@ func (svc *Service) createRollbackDeployment(
return nil, fmt.Errorf("failed to create rollback deployment: %w", saveErr)
}
_ = deployment.AppendLog(ctx, "Rolling back to previous image: "+app.PreviousImageID.String)
_ = deployment.AppendLog(
ctx,
"Rolling back to previous image: "+app.PreviousImageID.String,
)
return deployment, nil
}
@@ -417,7 +430,11 @@ func (svc *Service) executeRollback(
svc.removeOldContainer(ctx, app, deployment)
rollbackOpts, err := svc.buildContainerOptions(ctx, app, docker.ImageID(previousImageID))
rollbackOpts, err := svc.buildContainerOptions(
ctx,
app,
docker.ImageID(previousImageID),
)
if err != nil {
svc.failDeployment(bgCtx, app, deployment, err)
@@ -426,7 +443,12 @@ func (svc *Service) executeRollback(
containerID, err := svc.docker.CreateContainer(ctx, rollbackOpts)
if err != nil {
svc.failDeployment(bgCtx, app, deployment, fmt.Errorf("failed to create rollback container: %w", err))
svc.failDeployment(
bgCtx,
app,
deployment,
fmt.Errorf("failed to create rollback container: %w", err),
)
return fmt.Errorf("failed to create rollback container: %w", err)
}
@@ -436,7 +458,12 @@ func (svc *Service) executeRollback(
startErr := svc.docker.StartContainer(ctx, containerID)
if startErr != nil {
svc.failDeployment(bgCtx, app, deployment, fmt.Errorf("failed to start rollback container: %w", startErr))
svc.failDeployment(
bgCtx,
app,
deployment,
fmt.Errorf("failed to start rollback container: %w", startErr),
)
return fmt.Errorf("failed to start rollback container: %w", startErr)
}
@@ -695,7 +722,11 @@ func (svc *Service) cleanupCancelledDeploy(
if removeErr != nil {
svc.log.Error("failed to remove image from cancelled deploy",
"error", removeErr, "app", app.Name, "image", imageID)
_ = deployment.AppendLog(ctx, "WARNING: failed to clean up image "+imageID.String()+": "+removeErr.Error())
_ = deployment.AppendLog(
ctx,
"WARNING: failed to clean up image "+
imageID.String()+": "+removeErr.Error(),
)
} else {
svc.log.Info("cleaned up image from cancelled deploy",
"app", app.Name, "image", imageID)
@@ -870,14 +901,24 @@ func (svc *Service) cloneRepository(
err := os.MkdirAll(appBuildsDir, buildsDirPermissions)
if err != nil {
svc.failDeployment(ctx, app, deployment, fmt.Errorf("failed to create builds dir: %w", err))
svc.failDeployment(
ctx,
app,
deployment,
fmt.Errorf("failed to create builds dir: %w", err),
)
return "", nil, fmt.Errorf("failed to create builds dir: %w", err)
}
buildDir, err := os.MkdirTemp(appBuildsDir, fmt.Sprintf("%d-*", deployment.ID))
if err != nil {
svc.failDeployment(ctx, app, deployment, fmt.Errorf("failed to create temp dir: %w", err))
svc.failDeployment(
ctx,
app,
deployment,
fmt.Errorf("failed to create temp dir: %w", err),
)
return "", nil, fmt.Errorf("failed to create temp dir: %w", err)
}
@@ -908,7 +949,12 @@ func (svc *Service) cloneRepository(
)
if cloneErr != nil {
cleanup()
svc.failDeployment(ctx, app, deployment, fmt.Errorf("failed to clone repo: %w", cloneErr))
svc.failDeployment(
ctx,
app,
deployment,
fmt.Errorf("failed to clone repo: %w", cloneErr),
)
return "", nil, fmt.Errorf("failed to clone repo: %w", cloneErr)
}

View File

@@ -32,7 +32,10 @@ func TestCleanupCancelledDeploy_RemovesBuildDir(t *testing.T) {
require.NoError(t, os.MkdirAll(deployDir, 0o750))
// Create a file inside to verify full removal
require.NoError(t, os.WriteFile(filepath.Join(deployDir, "work"), []byte("test"), 0o600))
require.NoError(
t,
os.WriteFile(filepath.Join(deployDir, "work"), []byte("test"), 0o600),
)
// Also create a dir for a different deployment (should NOT be removed)
otherDir := filepath.Join(buildDir, "99-xyz789")

View File

@@ -31,7 +31,9 @@ func TestBuildContainerOptionsUsesImageID(t *testing.T) {
const expectedImageID = docker.ImageID("sha256:abc123def456")
opts, err := svc.BuildContainerOptionsExported(context.Background(), app, expectedImageID)
opts, err := svc.BuildContainerOptionsExported(
context.Background(), app, expectedImageID,
)
if err != nil {
t.Fatalf("buildContainerOptions returned error: %v", err)
}
@@ -77,14 +79,20 @@ func TestBuildContainerOptionsNoResourceLimits(t *testing.T) {
}
}
func TestBuildContainerOptionsCPULimit(t *testing.T) {
t.Parallel()
// buildOptsForApp saves an app configured by setup and returns the container
// options built for it.
func buildOptsForApp(
t *testing.T,
name string,
setup func(app *models.App),
) docker.CreateContainerOptions {
t.Helper()
db := database.NewTestDatabase(t)
app := models.NewApp(db)
app.Name = "cpulimit"
app.CPULimit = sql.NullFloat64{Float64: 0.5, Valid: true}
app.Name = name
setup(app)
err := app.Save(context.Background())
if err != nil {
@@ -101,6 +109,16 @@ func TestBuildContainerOptionsCPULimit(t *testing.T) {
t.Fatalf("buildContainerOptions returned error: %v", err)
}
return opts
}
func TestBuildContainerOptionsCPULimit(t *testing.T) {
t.Parallel()
opts := buildOptsForApp(t, "cpulimit", func(app *models.App) {
app.CPULimit = sql.NullFloat64{Float64: 0.5, Valid: true}
})
if opts.CPULimit != 0.5 {
t.Errorf("expected CPULimit=0.5, got %v", opts.CPULimit)
}
@@ -109,26 +127,9 @@ func TestBuildContainerOptionsCPULimit(t *testing.T) {
func TestBuildContainerOptionsMemoryLimit(t *testing.T) {
t.Parallel()
db := database.NewTestDatabase(t)
app := models.NewApp(db)
app.Name = "memlimit"
app.MemoryLimit = sql.NullInt64{Int64: 536870912, Valid: true} // 512m
err := app.Save(context.Background())
if err != nil {
t.Fatalf("failed to save app: %v", err)
}
log := slog.New(slog.NewTextHandler(os.Stderr, nil))
svc := deploy.NewTestService(log)
opts, err := svc.BuildContainerOptionsExported(
context.Background(), app, docker.ImageID("test:latest"),
)
if err != nil {
t.Fatalf("buildContainerOptions returned error: %v", err)
}
opts := buildOptsForApp(t, "memlimit", func(app *models.App) {
app.MemoryLimit = sql.NullInt64{Int64: 536870912, Valid: true} // 512m
})
if opts.MemoryLimit != 536870912 {
t.Errorf("expected MemoryLimit=536870912, got %v", opts.MemoryLimit)

View File

@@ -26,7 +26,11 @@ func (svc *Service) CancelActiveDeploy(appID string) {
}
// RegisterActiveDeploy registers an active deploy for testing.
func (svc *Service) RegisterActiveDeploy(appID string, cancel context.CancelFunc, done chan struct{}) {
func (svc *Service) RegisterActiveDeploy(
appID string,
cancel context.CancelFunc,
done chan struct{},
) {
svc.activeDeploys.Store(appID, &activeDeploy{cancel: cancel, done: done})
}
@@ -41,7 +45,11 @@ func (svc *Service) UnlockApp(appID string) {
}
// NewTestServiceWithConfig creates a Service with config and docker client for testing.
func NewTestServiceWithConfig(log *slog.Logger, cfg *config.Config, dockerClient *docker.Client) *Service {
func NewTestServiceWithConfig(
log *slog.Logger,
cfg *config.Config,
dockerClient *docker.Client,
) *Service {
return &Service{
log: log,
config: cfg,