From bd34e258ed865ffa0356edbd75f494ab9a97c2b3 Mon Sep 17 00:00:00 2001 From: Lukas Wuttke Date: Tue, 14 Jul 2026 14:23:32 +0200 Subject: [PATCH 1/3] ci: goimports -local gate + Dependabot config (gomod weekly, actions monthly) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit goimports -local github.com/tracebloc/cli now blocks in both loops: build.yml's lint job (pinned goimports@v0.48.0, same x/tools version as the deadcode pin) and the Makefile's fmt-check target, with make fmt extended to auto-fix. .golangci.yml already declared this grouping via local-prefixes but nothing enforced it — 4 files had drifted (data.go, data_test.go, ingestion_run_test.go, resources_set_test.go), fixed here with import-grouping-only diffs. .github/dependabot.yml extends the org's backend-only Dependabot pilot: gomod weekly (k8s.io/* + sigs.k8s.io/* grouped, golang.org/x/* grouped), github-actions monthly. Unlike backend's security-only config, the CLI takes real version updates — customers install this binary, so staying current is security posture (see #276). Org-wide rollout decision flagged to Asad on the PR. Co-Authored-By: Claude Fable 5 --- .github/dependabot.yml | 41 ++++++++++++++++++++++++++++++ .github/workflows/build.yml | 14 ++++++++++ Makefile | 11 ++++++++ internal/cli/data_test.go | 9 ++++--- internal/cli/ingestion_run_test.go | 5 ++-- internal/cli/resources_set_test.go | 3 ++- 6 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..cdfeaa04 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,41 @@ +# Dependabot config for tracebloc/cli — extends the org's backend-only +# pilot (backend runs security-only updates; see tracebloc/backend +# .github/dependabot.yml) to a second repo. Unlike backend, this repo +# takes real VERSION updates: the CLI ships as a customer-installed +# binary, so staying current on k8s.io/* and golang.org/x/* is a +# security posture, not just hygiene (see #276 — six reachable vulns +# rode a stale x/net + toolchain). +# +# gomod is weekly and grouped so the k8s.io/* constellation (which only +# upgrades cleanly in lockstep) and the golang.org/x/* siblings arrive +# as one PR each instead of a dozen singletons. github-actions is +# monthly — workflow pins move rarely and reviews are trivial. +# +# Org-wide rollout beyond backend + cli is an open decision — flagged +# to @saadqbal on the PR that added this file. + +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + labels: + - "work-type:tech-debt" + - "dependencies" + groups: + k8s: + patterns: + - "k8s.io/*" + - "sigs.k8s.io/*" + golang-x: + patterns: + - "golang.org/x/*" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + labels: + - "work-type:tech-debt" + - "dependencies" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3f0049dc..4b76f3a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -129,6 +129,20 @@ jobs: exit 1 fi + # goimports -local: enforce the stdlib / third-party / our-own import + # grouping that .golangci.yml's local-prefixes already declares. gofmt + # doesn't check grouping, so drift accumulated silently until now. + - name: goimports -local + run: | + go install golang.org/x/tools/cmd/goimports@v0.48.0 + drift="$(goimports -local github.com/tracebloc/cli -l .)" + if [ -n "$drift" ]; then + echo "::error::goimports (import grouping) drift in:" + echo "$drift" | sed 's/^/ /' + echo "::error::run \`make fmt\` to fix" + exit 1 + fi + - name: ineffassign run: | go install github.com/gordonklaus/ineffassign@v0.2.0 diff --git a/Makefile b/Makefile index 29ff5271..0130d21d 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ MISSPELL_VERSION ?= v0.3.4 DEADCODE_VERSION ?= v0.48.0 GOVULNCHECK_VERSION ?= v1.1.4 STATICCHECK_VERSION ?= 2025.1.1 +GOIMPORTS_VERSION ?= v0.48.0 # ---- top-level targets ------------------------------------------- @@ -140,7 +141,10 @@ lint-full: .PHONY: fmt fmt: gofmt -s -w . + $(GO) run golang.org/x/tools/cmd/goimports@$(GOIMPORTS_VERSION) -local github.com/tracebloc/cli -w . +# fmt-check: gofmt -s (simplification) + goimports -local (import grouping: +# stdlib / third-party / our own — matches .golangci.yml's local-prefixes). .PHONY: fmt-check fmt-check: @diff="$$(gofmt -s -l . 2>/dev/null)"; \ @@ -150,6 +154,13 @@ fmt-check: echo "==> run \`make fmt\` to fix"; \ exit 1; \ fi + @drift="$$($(GO) run golang.org/x/tools/cmd/goimports@$(GOIMPORTS_VERSION) -local github.com/tracebloc/cli -l .)"; \ + if [ -n "$$drift" ]; then \ + echo "==> goimports (import grouping) needed on:"; \ + echo "$$drift" | sed 's/^/ /'; \ + echo "==> run \`make fmt\` to fix"; \ + exit 1; \ + fi .PHONY: schema-check schema-check: diff --git a/internal/cli/data_test.go b/internal/cli/data_test.go index 79caf995..aabbe6bd 100644 --- a/internal/cli/data_test.go +++ b/internal/cli/data_test.go @@ -1,22 +1,25 @@ package cli import ( - "github.com/tracebloc/cli/internal/push" - "github.com/tracebloc/cli/internal/ui" "image" "image/png" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "github.com/tracebloc/cli/internal/push" + "github.com/tracebloc/cli/internal/ui" + "bytes" "context" "errors" - "github.com/tracebloc/cli/internal/cluster" "os" "path/filepath" "strconv" "strings" "testing" + + "github.com/tracebloc/cli/internal/cluster" ) // imgcLayout drops a minimum-viable image_classification directory diff --git a/internal/cli/ingestion_run_test.go b/internal/cli/ingestion_run_test.go index cf4639a7..2cb33b41 100644 --- a/internal/cli/ingestion_run_test.go +++ b/internal/cli/ingestion_run_test.go @@ -10,12 +10,13 @@ import ( "strings" "testing" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "github.com/tracebloc/cli/internal/cluster" "github.com/tracebloc/cli/internal/push" "github.com/tracebloc/cli/internal/submit" "github.com/tracebloc/cli/internal/ui" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" ) // The money path (#1009): submit → classify → exit-code → JSON → reclaim. diff --git a/internal/cli/resources_set_test.go b/internal/cli/resources_set_test.go index 974cdcd8..99324909 100644 --- a/internal/cli/resources_set_test.go +++ b/internal/cli/resources_set_test.go @@ -7,10 +7,11 @@ import ( "strings" "testing" + "k8s.io/client-go/kubernetes/fake" + "github.com/tracebloc/cli/internal/cluster" "github.com/tracebloc/cli/internal/helm" "github.com/tracebloc/cli/internal/ui" - "k8s.io/client-go/kubernetes/fake" ) // setTarget builds a resolved cluster target from a fake clientset, with a chart From 6f341286d58e8f969d26cbcfe8c45553da1a919b Mon Sep 17 00:00:00 2001 From: Lukas Wuttke Date: Tue, 14 Jul 2026 16:40:14 +0200 Subject: [PATCH 2/3] ci: canonicalize import grouping in data_test.go for the goimports gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regroups imports into the layout goimports -local github.com/tracebloc/cli emits (stdlib / third-party / tracebloc-local). gofmt and goimports only sort within existing blank-line groups, so the manual groups passed fmt-check as-is but were not the canonical single-block output; Bugbot and our precheck both flagged the divergence. Verified idempotent under the pinned goimports v0.48.0 and green on make fmt-check + go build ./... Rebased onto ci/279 (post-#303 data.go split): data.go's import block is already canonical from the #303 split, so its canonicalization here is a no-op and dropped — this commit now regroups data_test.go only. Co-Authored-By: Claude Fable 5 --- internal/cli/data_test.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/cli/data_test.go b/internal/cli/data_test.go index aabbe6bd..2a0df43e 100644 --- a/internal/cli/data_test.go +++ b/internal/cli/data_test.go @@ -1,25 +1,23 @@ package cli import ( - "image" - "image/png" - - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - - "github.com/tracebloc/cli/internal/push" - "github.com/tracebloc/cli/internal/ui" - "bytes" "context" "errors" + "image" + "image/png" "os" "path/filepath" "strconv" "strings" "testing" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "github.com/tracebloc/cli/internal/cluster" + "github.com/tracebloc/cli/internal/push" + "github.com/tracebloc/cli/internal/ui" ) // imgcLayout drops a minimum-viable image_classification directory From c556c9c37c5e83c0fe46b9a20b8c929ece95a52c Mon Sep 17 00:00:00 2001 From: Lukas Wuttke Date: Tue, 14 Jul 2026 14:34:41 +0200 Subject: [PATCH 3/3] ci: flip deadcode gate to blocking; delete the 3 dead #127 leftovers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deadcode CI step loses continue-on-error (and the Makefile target its '|| true'): both now run scripts/deadcode-check.sh, which fails on any function unreachable from ./cmd/tracebloc that isn't declared in scripts/deadcode-allowlist.txt. The tool itself always exits 0, so the old advisory step never could have blocked — the gate keys on output. Deleted (verified still dead with the pinned deadcode@v0.48.0): - config.clearAll + its three dedicated tests (TestClearAll, TestClearAll_HomeError, TestClear) — logout uses Save, nothing else ever called it - push.allCategoryIDs — moved verbatim into category_registry_test.go (the registry-pinning tests legitimately iterate every id; the shipped binary shouldn't carry the helper) - submit.isSubmitError — moved into client_test.go as the test-local assertion helper it always was (orphaned 'errors' import dropped) Allowlisted with reasons (the 4 legit findings): Status.String + JobOutcome.String (fmt-reflection Stringers) and ReadLabelValues + inferColumnType (di#349 test-only parity harnesses). Stale allowlist entries warn without failing; line numbers are stripped so edits that shift code don't red the gate. Coverage floors still clear after the test deletions (cli 82.9% >= 68, submit 80.3% >= 72). Co-Authored-By: Claude Fable 5 --- .github/workflows/build.yml | 23 +++++------ Makefile | 16 ++++---- internal/config/config.go | 15 ------- internal/config/config_coverage_test.go | 9 +---- internal/config/config_test.go | 14 ------- internal/config/fault_test.go | 40 ------------------- internal/push/category.go | 10 ----- internal/push/category_registry_test.go | 11 ++++++ internal/submit/client.go | 10 ----- internal/submit/client_test.go | 9 +++++ scripts/deadcode-allowlist.txt | 17 ++++++++ scripts/deadcode-check.sh | 52 +++++++++++++++++++++++++ 12 files changed, 107 insertions(+), 119 deletions(-) create mode 100644 scripts/deadcode-allowlist.txt create mode 100755 scripts/deadcode-check.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4b76f3a5..6dca66e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -163,19 +163,16 @@ jobs: go install honnef.co/go/tools/cmd/staticcheck@2025.1.1 staticcheck -checks all,-ST1005 ./... - # deadcode: reachability scan from the CLI entrypoint (~5s). ADVISORY - # (continue-on-error) for now — it reports unreachable funcs but does not - # fail the job. The module still carries pre-existing dead-ish funcs that - # are unsafe to delete blindly: Stringer methods (Status.String, - # JobOutcome.String) reached only via fmt reflection that static analysis - # can't see, plus test-only parity harnesses (ReadLabelValues, - # inferColumnType — di#349). Flip to blocking once that backlog is - # cleared. Tracked in #6 / #127. - - name: deadcode (advisory) - continue-on-error: true - run: | - go install golang.org/x/tools/cmd/deadcode@v0.48.0 - deadcode ./cmd/tracebloc + # deadcode: BLOCKING reachability scan from the CLI entrypoint (~5s). + # The four legit unreachables — Stringer methods (Status.String, + # JobOutcome.String) reached only via fmt reflection that static + # analysis can't see, plus the di#349 test-only parity harnesses + # (ReadLabelValues, inferColumnType) — are declared with reasons in + # scripts/deadcode-allowlist.txt. Anything else unreachable fails the + # job (#281 flipped this from advisory/continue-on-error). Tool version + # pinned inside the script; DEADCODE_VERSION overrides. + - name: deadcode + run: ./scripts/deadcode-check.sh govulncheck: timeout-minutes: 10 diff --git a/Makefile b/Makefile index 0130d21d..8228135e 100644 --- a/Makefile +++ b/Makefile @@ -107,17 +107,15 @@ lint: $(GO) run github.com/client9/misspell/cmd/misspell@$(MISSPELL_VERSION) -error . $(GO) run honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VERSION) -checks all,-ST1005 ./... -# deadcode: reachability scan from the CLI entrypoint (~5s). ADVISORY for now -# (non-blocking) — it prints unreachable funcs but never fails the build. The -# module still carries pre-existing dead-ish funcs that are unsafe to delete -# blindly: Stringer methods (Status.String, JobOutcome.String) reached only via -# fmt reflection that static analysis can't see, plus test-only parity harnesses -# (ReadLabelValues, inferColumnType — di#349). Flip to blocking once that -# backlog is cleared. Tracked in tracebloc/cli#6 / #127. +# deadcode: BLOCKING reachability scan from the CLI entrypoint (~5s). The four +# legit unreachables — Stringer methods (Status.String, JobOutcome.String) +# reached only via fmt reflection that static analysis can't see, plus the +# di#349 test-only parity harnesses (ReadLabelValues, inferColumnType) — are +# declared in scripts/deadcode-allowlist.txt with reasons. Anything else +# unreachable fails the build (#281 flipped this from advisory). .PHONY: deadcode deadcode: - @echo "==> deadcode (advisory): unreachable funcs from ./cmd/tracebloc" - @$(GO) run golang.org/x/tools/cmd/deadcode@$(DEADCODE_VERSION) ./cmd/tracebloc || true + @DEADCODE_VERSION=$(DEADCODE_VERSION) ./scripts/deadcode-check.sh # vulncheck: govulncheck reachability scan for known CVEs (stdlib + deps). # BLOCKING — this is a customer-installed binary; v0.8.0 shipped with 6 diff --git a/internal/config/config.go b/internal/config/config.go index b4dc08dd..5490ff3d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -234,18 +234,3 @@ func (c *Config) Save() error { } return nil } - -// clearAll removes the config file (full sign-out + reset, all envs). A missing -// file is not an error. Unexported: no production caller (logout uses Save); -// retained only for the same-package test that pins the remove-and-tolerate- -// missing behavior. (`clear` is a Go builtin, hence clearAll.) -func clearAll() error { - path, err := Path() - if err != nil { - return err - } - if err := os.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) { - return fmt.Errorf("removing %s: %w", path, err) - } - return nil -} diff --git a/internal/config/config_coverage_test.go b/internal/config/config_coverage_test.go index 4cd908cb..c5d1d1f1 100644 --- a/internal/config/config_coverage_test.go +++ b/internal/config/config_coverage_test.go @@ -9,7 +9,7 @@ import ( // clearHomeAndConfigDir removes every source of a config dir so Dir()/Path() // fail — the lever that covers the error-propagation branches of Dir, Path, -// Load, Save and clearAll (os.UserHomeDir errors when $HOME is empty). +// Load and Save (os.UserHomeDir errors when $HOME is empty). func clearHomeAndConfigDir(t *testing.T) { t.Helper() t.Setenv("TRACEBLOC_CONFIG_DIR", "") @@ -148,10 +148,3 @@ func TestSave_ErrorBranches(t *testing.T) { } }) } - -func TestClearAll_HomeError(t *testing.T) { - clearHomeAndConfigDir(t) - if err := clearAll(); err == nil { - t.Error("clearAll() must propagate Path()'s error") - } -} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index afc6c7de..428a5b3e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -55,20 +55,6 @@ func TestLoadMissingIsEmpty(t *testing.T) { } } -func TestClear(t *testing.T) { - t.Setenv("TRACEBLOC_CONFIG_DIR", t.TempDir()) - _ = (&Config{CurrentEnv: "prod", Profiles: map[string]*Profile{"prod": {Token: "x"}}}).Save() - if err := clearAll(); err != nil { - t.Fatal(err) - } - if c, _ := Load(); c.SignedIn() { - t.Error("after clearAll, should not be signed in") - } - if err := clearAll(); err != nil { - t.Errorf("clearAll on a missing file should be nil, got %v", err) - } -} - // TestMigrateV1ToV2 pins the v1 (flat cli#83 schema) → v2 migration: the single // record is wrapped under profiles[env], no data loss, and the next Save rewrites // the file as v2 on disk. diff --git a/internal/config/fault_test.go b/internal/config/fault_test.go index bc3f92af..b4aafdd7 100644 --- a/internal/config/fault_test.go +++ b/internal/config/fault_test.go @@ -91,43 +91,3 @@ func TestSave_Faults(t *testing.T) { } }) } - -// TestClearAll pins clearAll (config.go:242, was 67%): remove the config, -// tolerate a missing file, and surface a genuine remove failure. -func TestClearAll(t *testing.T) { - t.Run("removes an existing config", func(t *testing.T) { - dir := t.TempDir() - t.Setenv("TRACEBLOC_CONFIG_DIR", dir) - path := filepath.Join(dir, "config.json") - if err := os.WriteFile(path, []byte("{}"), 0o600); err != nil { - t.Fatal(err) - } - if err := clearAll(); err != nil { - t.Fatalf("clearAll: %v", err) - } - if _, err := os.Stat(path); !os.IsNotExist(err) { - t.Error("config must be gone after clearAll") - } - }) - t.Run("missing config is a no-op", func(t *testing.T) { - t.Setenv("TRACEBLOC_CONFIG_DIR", t.TempDir()) - if err := clearAll(); err != nil { - t.Errorf("a missing config must clear cleanly, got %v", err) - } - }) - t.Run("un-removable path -> error", func(t *testing.T) { - dir := t.TempDir() - t.Setenv("TRACEBLOC_CONFIG_DIR", dir) - // config.json as a NON-EMPTY directory → os.Remove refuses it. - cfgDir := filepath.Join(dir, "config.json") - if err := os.Mkdir(cfgDir, 0o700); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(cfgDir, "child"), []byte("x"), 0o600); err != nil { - t.Fatal(err) - } - if err := clearAll(); err == nil { - t.Error("a non-empty directory at the config path must surface a remove error") - } - }) -} diff --git a/internal/push/category.go b/internal/push/category.go index 7a8df80f..cd7f2a2d 100644 --- a/internal/push/category.go +++ b/internal/push/category.go @@ -299,16 +299,6 @@ func SupportedCategoryIDs() []string { return ids } -// allCategoryIDs returns every recognized category id, in registry order. -// Unexported: only the same-package registry test consumes it. -func allCategoryIDs() []string { - ids := make([]string, 0, len(categoryRegistry)) - for _, c := range categoryRegistry { - ids = append(ids, c.ID) - } - return ids -} - // SupportedCategoriesList is the comma-joined supported ids, for help text // and gate error messages. func SupportedCategoriesList() string { return strings.Join(SupportedCategoryIDs(), ", ") } diff --git a/internal/push/category_registry_test.go b/internal/push/category_registry_test.go index 67bc29c5..5be0d36c 100644 --- a/internal/push/category_registry_test.go +++ b/internal/push/category_registry_test.go @@ -12,6 +12,17 @@ import ( // that the family predicates + the supported set all derive from it, so a // future edit can't reintroduce the "5 of 9" drift (cli#74). +// allCategoryIDs returns every recognized category id, in registry order. +// Test-local: production code iterates categoryRegistry directly, so this +// lives here to keep the shipped binary free of test-only helpers (#281). +func allCategoryIDs() []string { + ids := make([]string, 0, len(categoryRegistry)) + for _, c := range categoryRegistry { + ids = append(ids, c.ID) + } + return ids +} + func TestRegistryKnownCategories(t *testing.T) { want := []string{ "image_classification", "object_detection", "keypoint_detection", diff --git a/internal/submit/client.go b/internal/submit/client.go index e1eebab7..69747094 100644 --- a/internal/submit/client.go +++ b/internal/submit/client.go @@ -5,7 +5,6 @@ import ( "context" "crypto/tls" "encoding/json" - "errors" "fmt" "io" "net/http" @@ -184,12 +183,3 @@ func (e *SubmitError) Error() string { return fmt.Sprintf("jobs-manager %s returned HTTP %d: %s", e.Endpoint, e.StatusCode, strings.TrimSpace(e.Body)) } - -// isSubmitError reports whether err is a *SubmitError. Convenience -// for the orchestrator's exit-code mapping; errors.As would also -// work but this reads cleaner at the branch site. Unexported: only -// same-package tests reference it today. -func isSubmitError(err error) bool { - var se *SubmitError - return errors.As(err, &se) -} diff --git a/internal/submit/client_test.go b/internal/submit/client_test.go index 5ae2bbdd..3e21e07c 100644 --- a/internal/submit/client_test.go +++ b/internal/submit/client_test.go @@ -11,6 +11,15 @@ import ( "testing" ) +// isSubmitError reports whether err is a *SubmitError. Test-local +// assertion helper (also used by submit_test.go): production code has +// no caller, so it lives here to keep the shipped binary free of +// test-only helpers (#281). +func isSubmitError(err error) bool { + var se *SubmitError + return errors.As(err, &se) +} + // TestHTTPSubmitter_HappyPath: jobs-manager returns 201 with the // canonical body shape; client decodes correctly + surfaces all // three response fields. diff --git a/scripts/deadcode-allowlist.txt b/scripts/deadcode-allowlist.txt new file mode 100644 index 00000000..c4ae1811 --- /dev/null +++ b/scripts/deadcode-allowlist.txt @@ -0,0 +1,17 @@ +# Deadcode allowlist — the only unreachable-from-main findings the gate +# tolerates (scripts/deadcode-check.sh, cli#281). Every entry needs a reason. +# +# Format: ": " — deadcode's output with :line:col stripped, so +# ordinary edits that shift lines don't red the gate. Adding an entry here is +# a deliberate, reviewed act; prefer deleting the code. +# +# Stringer methods: reached only via fmt's reflection-driven %v/%s dispatch, +# which static reachability analysis can't see. Genuinely used. +internal/doctor/doctor.go: Status.String +internal/submit/watch.go: JobOutcome.String +# Test-only parity harnesses (di#349): mirror the ingestor's label reading / +# type inference so the parity tests can diff CLI vs ingestor behavior. They +# must live in production source (the tests compare against real package +# code), so they are unreachable from main by design. +internal/push/preflight.go: ReadLabelValues +internal/push/tabular.go: inferColumnType diff --git a/scripts/deadcode-check.sh b/scripts/deadcode-check.sh new file mode 100755 index 00000000..4e121585 --- /dev/null +++ b/scripts/deadcode-check.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Blocking deadcode gate (cli#281). Fail on any function unreachable from the +# CLI entrypoint that is not explicitly declared in deadcode-allowlist.txt. +# +# The x/tools deadcode command always exits 0, even with findings — so the +# gate keys on OUTPUT: normalize each finding to ": " (strip +# :line:col so ordinary edits don't red the gate), subtract the allowlist, +# and fail loudly on whatever's left. Stale allowlist entries (allowlisted +# funcs deadcode no longer reports) warn but don't fail — that's cleanup, +# not breakage. +# +# Usage: scripts/deadcode-check.sh (run from the repo root) +# DEADCODE_VERSION overrides the pinned tool version (Makefile passes it, +# keeping the single source of truth there — lockstep with build.yml). +# +# Portable to bash 3.2 (macOS default). +set -euo pipefail + +DEADCODE_VERSION="${DEADCODE_VERSION:-v0.48.0}" +allowlist_file="$(dirname "$0")/deadcode-allowlist.txt" + +raw="$(go run "golang.org/x/tools/cmd/deadcode@${DEADCODE_VERSION}" ./cmd/tracebloc)" + +# "path/file.go:12:6: unreachable func: Name" -> "path/file.go: Name" +findings="$(printf '%s' "$raw" | sed -E 's/^([^:]+):[0-9]+:[0-9]+: unreachable func: /\1: /')" + +# Allowlist minus comments/blanks. +allowed="$(grep -v '^[[:space:]]*#' "$allowlist_file" | grep -v '^[[:space:]]*$' || true)" + +unexpected="" +if [ -n "$findings" ]; then + unexpected="$(printf '%s\n' "$findings" | grep -vxF -e dummy-never-matches -f <(printf '%s\n' "$allowed") || true)" +fi + +stale="" +if [ -n "$allowed" ]; then + stale="$(printf '%s\n' "$allowed" | { grep -vxF -f <(printf '%s\n' "$findings") || true; })" +fi + +if [ -n "$stale" ]; then + echo "==> deadcode: stale allowlist entries (no longer reported — consider pruning $allowlist_file):" + printf '%s\n' "$stale" | sed 's/^/ /' +fi + +if [ -n "$unexpected" ]; then + echo "==> deadcode: unreachable from ./cmd/tracebloc and NOT allowlisted:" + printf '%s\n' "$unexpected" | sed 's/^/ /' + echo "==> delete the code, or (deliberately, with a reason) add it to $allowlist_file" + exit 1 +fi + +echo "==> deadcode: clean (allowlist: $(printf '%s\n' "$allowed" | grep -c . ) entries)"