lint

package
v1.0.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: Apache-2.0 Imports: 81 Imported by: 0

Documentation

Overview

Package lint holds the `forge lint` command group — the project linter pipeline (golangci / buf / frontend / forge-convention / scaffold / migration-safety / wire-coverage …) plus the targeted single-rule flags and the --json aggregator.

It is a dir-nested command group (the devspace idiom). The substrate — the ~30 run*/collect* linter entry points and the shared linterStep table — moved here verbatim from package cli so the command is the substrate's home, not a thin shim over package-cli internals. The few genuinely cross-cutting helpers it needs (project-store loader, the forge.yaml-not-found sentinel, the user-vs-maintainer flag split) come from the leaf packages internal/cli/factory and internal/cli/cmdutil, so the group never imports internal/cli (which would cycle — internal/cli blank-imports the groups).

audit.go (now the internal/cli/audit group) reuses two of this package's collectors — CollectOptionalDepsGuardFindings and CollectConfigDepsFindings — via the exported aliases in lint_optional_deps_guard.go / lint_config_deps.go. That is a clean group→group dependency (audit → lint), no cycle.

File: internal/cli/lint/lint_flux_safe.go

flux-safe — an object that is wrong ONLY under a reconciler.

forge's direct apply is a one-shot; Flux re-applies the same bytes on every interval and prunes what a bundle stops carrying. So a Job with ttlSecondsAfterFinished is deleted by the TTL controller and created again by the next reconcile (a migration re-runs every period), and a Job whose name carries no hash of its spec fails every reconcile after its first change because a Job's spec is immutable. The rules, and the control-plane incidents behind them, are in pkg/release/fluxsafe.go; the bundle build refuses the same objects, so this is the same check run earlier, over every env.

SEVERITY FOLLOWS WHO APPLIES THE OBJECT'S DELIVERY UNIT. An object in a unit a reconciler applies gets an ERROR: its bundle would be refused at build. That is the whole env when it declares no lifecycle, no applier and no hub_converges, and only the listed units when it declares hub_converges. An object forge applies itself gets a WARNING: nothing re-applies it today, but it cannot move onto Flux until the object is fixed, and saying so now is cheaper than finding out in the migration. Forge-applied is a transitional state, so the warning is the migration's to-do list. A render that fails here is skipped, as in the PDB lint: lint judges the project and `forge env render` owns render errors.

`forge lint --generated-drift` — the gate on hand-edited generated files.

── Why a lint gate exists at all ─────────────────────────────────────

forge already DETECTS this: every Tier-1 (regenerated-every-run) file carries a `// Code generated by forge. DO NOT EDIT.` banner plus its own content hash, and generator.ScanProjectDrift reports every file whose bytes no longer match that hash. Until this step, though, the only surfaces that acted on the signal were `forge generate` (the pre-pipeline stomp guard) and `forge ci verify-generated`, which leaves a real hole:

  • the stomp guard fires AT generate time, which is by definition the moment the edit is about to be destroyed. Everything between the edit and the next generate is silent.
  • the guard also SCOPES its drift set to the emitters that would run this invocation (generate_tier1_scope.go). Drift in an out-of-scope file is a warning, not a failure — so an edit to, say, pkg/app/testing.go under a `--steps=…` run is waved through.
  • `forge project audit` reports the same drift, but audit is a non-gating snapshot by design: it always exits 0.

So an agent (or a human) could hand-edit a DO-NOT-EDIT file, watch every check pass, commit it, and lose the work at the next regenerate. `forge lint` is the check that runs BETWEEN edits and generate — in the agent loop and in the CI lint job — so that is where the gate belongs. It scans the whole tree with no emitter-scope filter: if forge owns the bytes and the bytes changed, lint fails.

── No false positives ────────────────────────────────────────────────

The gate reports exactly what generator.ScanProjectDrift reports (see that file for the by-construction exclusions: scaffold-once files, disowned paths, files forge never wrote, pristine-older renders, formatter-only churn, Tier-2-managed starters), minus one more class handled here: the `unverified-legacy` sentinel. That marker means the one-time legacy-manifest migration could not ESTABLISH provenance — it is not evidence of an edit — so it is reported as a warning that routes the user to `forge generate`, never as a gating error.

A gate that cries wolf gets disabled, so every one of those exclusions is load-bearing.

File: internal/cli/lint/lint_identity_share.go

identity-share-with — the part of forge.ManagedIdentity.share_with that one env's render cannot see. KCL refuses a share_with that names another project, this env itself, or a host-run IdP, and a sharer that declares roles, smtp, seed users, machine users, a domain or a project name. Whether the OWNER env exists, declares a hosted identity of its own and is not itself a sharer (a chain) is a fact about two envs, so it is judged here, over every env of the project. The control plane re-checks it at admission, where it also sees the org.

File: internal/cli/lint/lint_pdb.go

pdb-blocks-drain — a PodDisruptionBudget that can never allow a disruption (minAvailable >= replicas, maxUnavailable 0, minAvailable 100%) blocks every node drain that touches its pods, forever. The check runs over each env's real render, so it covers forge-rendered PDBs and hand-written forge.Manifests alike. Warnings only: a render that fails here is skipped, since lint judges the project and `forge env render` owns render errors.

File: internal/cli/lint/lint_secrets.go

secrets — `forge lint --secrets`: the rules about platform secrets (store-backed forge.ExternalSecrets, the External Secrets Operator objects they render, forge.OpenBao grants) that kcl/schema.k cannot see because they span a whole rendered environment. The rules and what each one refuses are in internal/lint/secrets; this file renders each env, builds the facts, and reports.

GATING, with an escape in every message. A finding is an error unless the rule says otherwise (the escrow rule warns; the unreferenced-key tool is info), and every message names the KCL line and the directive that waives it.

OPT-IN BY ADOPTION. A project whose KCL declares no store source, OpenBao or ESO component is not rendered and not judged.

`forge lint --suggest-buf-excepts` walks the project's .proto files, runs `buf lint`, and prints a buf.yaml `lint.except:` snippet suggesting which STANDARD rules to silence for a port-in-progress codebase.

Migration projects (a Connect API spec ported from a pre-forge repo) routinely hit the same handful of STANDARD rules: PACKAGE_VERSION_SUFFIX when the original namespace was `foo.v1` vs the buf-canonical `foo.v1`, RPC_REQUEST_STANDARD_NAME when the RPCs predate the `<Method>Request` convention, FIELD_LOWER_SNAKE_CASE when fields were snake_case-with-acronyms, etc. The hint at the end of `forge lint` (printBufLintExceptHint) already nudges the user toward the four most common offenders, but the nudge is a static list — it can't tell which rules ACTUALLY fire in the user's tree.

This command runs `buf lint` and aggregates the output by rule name, then prints the rules that fired across more than the `--threshold` (default 3) files. The heuristic is intentionally conservative: a single .proto with one violation is probably a real bug to fix; the same violation across many files is almost always a port-wide convention mismatch best resolved via except.

The output is YAML-shaped so the user can paste it directly into buf.yaml. Nothing is mutated.

FRICTION 2026-06-02: cp-forge proto port — author hit FIELD_LOWER_SNAKE_CASE on 40+ files and had to grep buf output manually to count rule occurrences before deciding what to except.

Index

Constants

This section is empty.

Variables

View Source
var ErrProjectConfigNotFound = cmdutil.ErrProjectConfigNotFound

ErrProjectConfigNotFound is the canonical "forge.yaml not found" sentinel, re-exported from cmdutil so the lint helpers compare against the same value internal/cli's config.ErrProjectConfigNotFound aliases.

Functions

func GolangciLintModule added in v0.1.46

func GolangciLintModule() string

GolangciLintModule is the package `go install` builds golangci-lint from.

func GolangciLintOnPath added in v0.1.46

func GolangciLintOnPath(dir string) (path, why string)

GolangciLintOnPath reports the golangci-lint PATH resolves and whether it is forge's pin built with a Go that can analyze the module at dir. why says what is wrong with it ("" when nothing is); path is "" when there is none.

func GolangciLintPin added in v0.1.46

func GolangciLintPin() string

GolangciLintPin is the golangci-lint release forge lints with and installs.

func InstallGolangciLint added in v0.1.46

func InstallGolangciLint(ctx context.Context, dir, gobin string) (string, error)

InstallGolangciLint builds forge's pinned golangci-lint with the toolchain `go` selects for the module at dir, into gobin, and returns that toolchain. The build runs outside any module, so the project's go.mod and go.work cannot steer it.

func SetGateWriter added in v0.1.45

func SetGateWriter(w GateWriter)

SetGateWriter installs the gate writer. Called once, from the CLI package.

func WriteGateForTest added in v0.1.45

func WriteGateForTest(req GateRequest) error

WriteGateForTest exposes the installed gate writer so the CLI package can assert that --gate-json is actually wired. A test-only seam in production code, deliberately: the wiring happens in an init() in the CLI package, and nothing else can observe whether it ran.

Types

type ConfigDepsFinding

type ConfigDepsFinding = configDepsFinding

ConfigDepsFinding is the exported alias for the config-deps finding, consumed by the internal/cli/audit group's auditConfigDeps roll-up. A type alias (not a new type) so audit's field access (.Role/.Package/.File/.Line/.Field/.Type) is unchanged from when both commands lived in package cli.

func CollectConfigDepsFindings

func CollectConfigDepsFindings(projectDir string) ([]ConfigDepsFinding, error)

CollectConfigDepsFindings is the exported entry the audit group calls. It forwards to the package-internal collector; the audit roll-up is the only cross-package consumer.

type GateRequest added in v0.1.45

type GateRequest struct {
	// Path is where the document is written.
	Path string
	// Name is the check: "lint".
	Name string
	// Status is the verdict.
	Status string
	// Summary is the one line a reader sees.
	Summary string
	// Details is small and structured — counts, never logs.
	Details map[string]any
	// StartedAt and FinishedAt are the CHECK's own window, which is what
	// makes a gate a run stage with a duration.
	StartedAt  time.Time
	FinishedAt time.Time
}

GateRequest is one completed check, as the CLI needs it to write a gate document. A struct rather than six parameters because it crosses a package boundary and every field is optional-ish detail about the same event.

type GateWriter added in v0.1.45

type GateWriter func(req GateRequest) error

GateWriter writes a gate document.

Declared HERE, at the consumer, rather than exported from the CLI: this package needs "something that can write a gate", and that one-method contract is all it is told about the gate vocabulary.

type OptionalDepsGuardFinding

type OptionalDepsGuardFinding = optionalDepsGuardFinding

OptionalDepsGuardFinding is the exported alias for the optional-deps-guard finding, consumed by the internal/cli/audit group's auditOptionalDepsGuard roll-up. A type alias (not a new type) so audit's field access (.Role/.Package/.File/.Line/.Expr/.Method) is unchanged from when both commands lived in package cli.

func CollectOptionalDepsGuardFindings

func CollectOptionalDepsGuardFindings(projectDir string) ([]OptionalDepsGuardFinding, error)

CollectOptionalDepsGuardFindings is the exported entry the audit group calls. It forwards to the package-internal collector; the audit roll-up is the only cross-package consumer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL