Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: golangci-lint

on:
push:
branches: [master, main]
pull_request:
branches: [master, main]
schedule:
- cron: "23 6 * * 1"

permissions:
contents: read
pull-requests: read

jobs:
lint:
name: lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
path: worker
# Sibling checkouts (proto/common) for repos with replace directives.
# No-op for repos that do not need them.
- uses: actions/checkout@v4
if: ${{ hashFiles('worker/go.mod') != '' }}
with:
repository: InstaNode-dev/common
path: common
continue-on-error: true
- uses: actions/checkout@v4
with:
repository: InstaNode-dev/proto
path: proto
continue-on-error: true
- uses: actions/setup-go@v5
with:
go-version-file: worker/go.mod
- uses: golangci/golangci-lint-action@v8
with:
# Pinned (was `latest`) so a new upstream release can't silently
# introduce analyzers/findings that fail a previously-green PR —
# `latest` floated to v2.12.2 and turned on a govet `inline` check
# mid-PR. Bump deliberately + re-run `make gate` when upgrading.
version: v2.12.2
working-directory: worker
args: --timeout=5m
43 changes: 43 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# golangci-lint v2 config — start conservative, expand once baseline is clean
version: "2"

run:
timeout: 5m
tests: true

linters:
# Default linter set is govet+errcheck+ineffassign+staticcheck+unused.
# We explicitly add misspell + gocyclo on top. gosimple folded into staticcheck in v2.
enable:
- errcheck # checks unchecked errors
- govet # standard vet
- ineffassign # ineffective assignments
- staticcheck # bug detection (subsumes gosimple in v2)
- unused # unused code
- misspell # spelling
- gocyclo # cyclomatic complexity
settings:
gocyclo:
# Set just above the current top offender (33: entitlement_reconciler
# / event_email_forwarder Work funcs). These are inherently branchy
# job dispatch/reconcile loops; a config bump is preferred over a
# behaviour-changing refactor for this mechanical lint-gate pass.
min-complexity: 34
errcheck:
# The fmt.Fprint* family writes to stdout/stderr/buffers in CLI and
# diagnostic code where a write error is neither actionable nor
# meaningful. Matches the convention adopted by the sibling repos.
exclude-functions:
- fmt.Fprint
- fmt.Fprintf
- fmt.Fprintln
exclusions:
rules:
- path: _test\.go
linters:
- errcheck
- gocyclo

issues:
max-issues-per-linter: 0
max-same-issues: 0
2 changes: 1 addition & 1 deletion internal/email/brevo_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ func (p *BrevoProvider) doRequest(ctx context.Context, evt EventEmail, body []by
)
return "", &SendError{Class: SendClassTransient, Cause: err, Message: "brevo: http"}
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, brevoBodyReadCap))
statusLabel := strconv.Itoa(resp.StatusCode)

Expand Down
2 changes: 1 addition & 1 deletion internal/jobs/backup_extra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1592,7 +1592,7 @@ func TestDefaultPgDumpExec_StderrTruncates(t *testing.T) {
dir := t.TempDir()
bin := filepath.Join(dir, "noisy_pg_dump")
// Write 1000 bytes to stderr then exit 1.
script := fmt.Sprintf("#!/bin/sh\nfor i in $(seq 1 100); do printf 'XXXXXXXXXX' >&2; done\nexit 1\n")
script := "#!/bin/sh\nfor i in $(seq 1 100); do printf 'XXXXXXXXXX' >&2; done\nexit 1\n"
if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
t.Fatalf("write: %v", err)
}
Expand Down
10 changes: 5 additions & 5 deletions internal/jobs/billing_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ func (w *BillingReconcilerWorker) Work(ctx context.Context, job *river.Job[Billi
if err != nil {
return fmt.Errorf("BillingReconcilerWorker: query failed: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()

var teams []billingReconcilerTeamRow
for rows.Next() {
Expand All @@ -817,7 +817,7 @@ func (w *BillingReconcilerWorker) Work(ctx context.Context, job *river.Job[Billi
if rowsErr := rows.Err(); rowsErr != nil {
return fmt.Errorf("BillingReconcilerWorker: rows error: %w", rowsErr)
}
rows.Close()
_ = rows.Close()

metrics.BillingReconcilerTeamsScanned.Add(float64(len(teams)))

Expand Down Expand Up @@ -1071,7 +1071,7 @@ func (w *BillingReconcilerWorker) scanChargeUndeliverable(ctx context.Context) i
"error", err, "note", "fail-open — retry next tick")
return 0
}
defer rows.Close()
defer func() { _ = rows.Close() }()

var count int
var maxCreated time.Time
Expand Down Expand Up @@ -1176,7 +1176,7 @@ func (w *BillingReconcilerWorker) runOrphanSweep(ctx context.Context) (scanned,
)
return 0, 0
}
defer rows.Close()
defer func() { _ = rows.Close() }()

var candidates []billingReconcilerOrphanRow
for rows.Next() {
Expand All @@ -1191,7 +1191,7 @@ func (w *BillingReconcilerWorker) runOrphanSweep(ctx context.Context) (scanned,
slog.Warn("billing.reconciler.orphan_rows_error", "error", rowsErr)
return 0, 0
}
rows.Close()
_ = rows.Close()

for i, c := range candidates {
// 100ms stagger between Razorpay calls, same as the primary sweep.
Expand Down
4 changes: 2 additions & 2 deletions internal/jobs/checkout_reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func (w *CheckoutReconcileWorker) Work(ctx context.Context, job *river.Job[Check
if err != nil {
return fmt.Errorf("CheckoutReconcileWorker: candidate query failed: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()

var candidates []checkoutRow
for rows.Next() {
Expand All @@ -172,7 +172,7 @@ func (w *CheckoutReconcileWorker) Work(ctx context.Context, job *river.Job[Check
if err := rows.Err(); err != nil {
return fmt.Errorf("CheckoutReconcileWorker: rows error: %w", err)
}
rows.Close()
_ = rows.Close()

if len(candidates) == 0 {
// P1-1 (BugBash 2026-05-19): idle tick — zero candidates carries
Expand Down
10 changes: 2 additions & 8 deletions internal/jobs/churn_predictor.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,6 @@ type ChurnPredictorArgs struct{}
// Kind is the River worker key.
func (ChurnPredictorArgs) Kind() string { return "churn_predictor" }

// churnPredictorInterval is the periodic dispatch cadence — daily.
// The 30-day dedupe window means cadence below ~daily produces no
// additional flags; daily gives at most one chance per day to catch a
// team that crossed the 7-day threshold yesterday.
const churnPredictorInterval = 24 * time.Hour

// churnInactivityWindow is the silence period that defines a churn
// candidate. 7 days is the brief's threshold — long enough that a
// vacation or busy week doesn't trigger a "we miss you" email
Expand Down Expand Up @@ -231,7 +225,7 @@ func (w *ChurnPredictorWorker) Work(ctx context.Context, job *river.Job[ChurnPre
if err != nil {
return fmt.Errorf("ChurnPredictorWorker: query failed: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()

var candidates []churnCandidateRow
for rows.Next() {
Expand All @@ -249,7 +243,7 @@ func (w *ChurnPredictorWorker) Work(ctx context.Context, job *river.Job[ChurnPre
if err := rows.Err(); err != nil {
return fmt.Errorf("ChurnPredictorWorker: rows error: %w", err)
}
rows.Close()
_ = rows.Close()

if len(candidates) == 0 {
slog.Info("jobs.churn_predictor.completed",
Expand Down
5 changes: 0 additions & 5 deletions internal/jobs/coverage_misc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -1277,10 +1276,6 @@ func TestProber_UptimeRetention_DBError_Propagates(t *testing.T) {
// ─── real_prober.go ────────────────────────────────────────────────────────────

func TestProber_NormalizeStorageURL_AllArms(t *testing.T) {
if _, err := url.Parse("://bad"); err == nil {
// just here to silence the unused-import linter if it ever fires
}

// http/https → returned untouched.
if got, err := normalizeStorageURL("https://example.com/bucket"); err != nil || got != "https://example.com/bucket" {
t.Errorf("https returned (%q, %v)", got, err)
Expand Down
4 changes: 2 additions & 2 deletions internal/jobs/custom_domain_reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ func (w *CustomDomainReconciler) reconcileCertReady(ctx context.Context, d activ
_ = w.updateLastCheck(ctx, d.id, fmt.Sprintf("HTTPS HEAD probe failed: %v", err))
return reconcileNoop
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode >= 200 && resp.StatusCode < 400 {
if err := w.updateStatus(ctx, d.id, statusLive, ""); err != nil {
Expand Down Expand Up @@ -366,7 +366,7 @@ func (w *CustomDomainReconciler) listActiveDomains(ctx context.Context) ([]activ
if err != nil {
return nil, fmt.Errorf("listActiveDomains: query: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()

var out []activeCustomDomain
for rows.Next() {
Expand Down
8 changes: 4 additions & 4 deletions internal/jobs/customer_backup_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ func (w *CustomerBackupRunnerWorker) Work(ctx context.Context, job *river.Job[Cu
if err != nil {
return fmt.Errorf("CustomerBackupRunnerWorker: select pending failed: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()

type pending struct {
backupID string
Expand All @@ -258,7 +258,7 @@ func (w *CustomerBackupRunnerWorker) Work(ctx context.Context, job *river.Job[Cu
if err := rows.Err(); err != nil {
return fmt.Errorf("CustomerBackupRunnerWorker: rows error: %w", err)
}
rows.Close()
_ = rows.Close()

processed := 0
succeeded := 0
Expand Down Expand Up @@ -695,7 +695,7 @@ func (w *CustomerBackupRunnerWorker) runRetentionSweep(ctx context.Context) {
}
victims = append(victims, v)
}
rows.Close()
_ = rows.Close()

for _, v := range victims {
if delErr := w.store.DeleteObject(ctx, w.bucket, v.s3Key); delErr != nil {
Expand Down Expand Up @@ -796,7 +796,7 @@ func (w *CustomerBackupRunnerWorker) refundManualBackupQuota(teamID uuid.UUID, b
}
return fmt.Errorf("api request: %w", doErr)
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
return fmt.Errorf("api status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
Expand Down
2 changes: 1 addition & 1 deletion internal/jobs/customer_backup_scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func (w *CustomerBackupSchedulerWorker) Work(ctx context.Context, job *river.Job
if err != nil {
return fmt.Errorf("CustomerBackupSchedulerWorker: query failed: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()

type cand struct {
id string
Expand Down
12 changes: 6 additions & 6 deletions internal/jobs/customer_restore_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ func (w *CustomerRestoreRunnerWorker) Work(ctx context.Context, job *river.Job[C
if err != nil {
return fmt.Errorf("CustomerRestoreRunnerWorker: select pending failed: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()

type pending struct {
restoreID string
Expand Down Expand Up @@ -189,7 +189,7 @@ func (w *CustomerRestoreRunnerWorker) Work(ctx context.Context, job *river.Job[C
if err := rows.Err(); err != nil {
return fmt.Errorf("CustomerRestoreRunnerWorker: rows error: %w", err)
}
rows.Close()
_ = rows.Close()

processed := 0
succeeded := 0
Expand Down Expand Up @@ -352,7 +352,7 @@ func (w *CustomerRestoreRunnerWorker) processRestore(parentCtx context.Context,

tmpFile, tmpErr := os.CreateTemp("", "instant-restore-*.dump.gz")
if tmpErr != nil {
obj.Close()
_ = obj.Close()
w.markRestoreFailed(ctx, p.restoreID, fmt.Sprintf("create temp file: %v", tmpErr), start, p)
return false
}
Expand All @@ -366,11 +366,11 @@ func (w *CustomerRestoreRunnerWorker) processRestore(parentCtx context.Context,
hasher := sha256.New()
tee := io.TeeReader(obj, hasher)
if _, copyErr := io.Copy(tmpFile, tee); copyErr != nil {
obj.Close()
_ = obj.Close()
w.markRestoreFailed(ctx, p.restoreID, fmt.Sprintf("S3 read failed: %v", copyErr), start, p)
return false
}
obj.Close()
_ = obj.Close()

// Integrity gate (same semantics as before — only the hashing path
// changed). The backup runner (customer_backup_runner.go, FIX-H #59)
Expand Down Expand Up @@ -412,7 +412,7 @@ func (w *CustomerRestoreRunnerWorker) processRestore(parentCtx context.Context,
w.markRestoreFailed(ctx, p.restoreID, fmt.Sprintf("gunzip header: %v", gzErr), start, p)
return false
}
defer gzReader.Close()
defer func() { _ = gzReader.Close() }()

if runErr := w.pgRestore.Run(ctx, plainConn, gzReader); runErr != nil {
w.markRestoreFailed(ctx, p.restoreID, fmt.Sprintf("pg_restore failed: %v", runErr), start, p)
Expand Down
2 changes: 1 addition & 1 deletion internal/jobs/deploy_failure_autopsy.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ func (c *k8sAutopsyClient) GetPodLogs(ctx context.Context, namespace, podName st
return nil, nil //nolint:nilerr
}
}
defer stream.Close()
defer func() { _ = stream.Close() }()
return readLogLines(stream)
}

Expand Down
4 changes: 2 additions & 2 deletions internal/jobs/deploy_notify_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ func (w *DeployNotifyWebhookWorker) fetchBatch(ctx context.Context, c deployNoti
if err != nil {
return nil, fmt.Errorf("fetchBatch query: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()

var out []deployNotifyAuditRow
for rows.Next() {
Expand Down Expand Up @@ -569,7 +569,7 @@ func (w *DeployNotifyWebhookWorker) dispatch(ctx context.Context, urlStr string,
// Drain + close body unconditionally so the connection
// returns to the keep-alive pool.
_, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close()
_ = resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion internal/jobs/deploy_status_reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ func (w *DeployStatusReconciler) listActiveDeployments(ctx context.Context) ([]a
if err != nil {
return nil, fmt.Errorf("listActiveDeployments: query: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()

var out []activeDeployment
for rows.Next() {
Expand Down
4 changes: 2 additions & 2 deletions internal/jobs/deployment_expirer.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (w *DeploymentExpirerWorker) Work(ctx context.Context, job *river.Job[Deplo
if err != nil {
return fmt.Errorf("DeploymentExpirerWorker: query failed: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()

var candidates []deployExpirerRow
for rows.Next() {
Expand All @@ -119,7 +119,7 @@ func (w *DeploymentExpirerWorker) Work(ctx context.Context, job *river.Job[Deplo
if err := rows.Err(); err != nil {
return fmt.Errorf("DeploymentExpirerWorker: rows error: %w", err)
}
rows.Close()
_ = rows.Close()

if len(candidates) == 0 {
// T21 P1-1 (BugBash 2026-05-20): idle-tick demoted INFO→DEBUG.
Expand Down
Loading
Loading