Update golangci-lint to v2.12.2 with canonical config (#187)
All checks were successful
Check / check (push) Successful in 4s
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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user