Skip to main content
A forge release can add a lint rule or tighten an old one. If every new rule failed your build the day you bumped the pin, upgrading forge would also be the day you learned about two hundred findings you did not write. Forge avoids that. A new or tightened rule warns for one release, then gates in the next. The pin in your forge.yaml decides which side of that line you are on.

The two versions on every rule

Every rule that lint.rules can grade has a row in forge’s rollout table (internal/cli/lint/lint_rollout_table.go) with two release numbers: Rules that shipped at or before v0.1.46 have no gates_from: they gate from the first release you can pin. Every rule introduced after that declares both. A rule currently in its grace period reads v1.0.0 -> v1.0.1 in the rule reference; an older rule reads just its release. Your forge.yaml carries forge_version. Lint compares it with gates_from: Only a finding that would have failed the gate is softened. A warning stays an ordinary warning with no note, and a rule at or before v0.1.46 is never softened. A pseudo-version such as v1.0.14-0.20261102231925-a88d3c30f302 sorts below the release it leads to, which is the right reading: a project pinned between releases has not been through a release with the rule yet.

What you see

This is a real run. The scratch project has an slog.Info call inside a function that has a ctx, which forge-tracing-log-without-ctx flags. The pin is v1.0.0, one release before the rule gates (v1.0.1). The finding is a warning, it carries the note that explains why, and the run exits 0:
The verdict counts these findings and names them, so a green run never hides that a pin bump will turn it red. They do not fail the lane, the verdict, the exit code, or --gate-json. With --quiet the note is hidden along with the other warnings, but the verdict line still counts them. With --json the same facts are on the finding as a grace object:
fix names the forge fix codemod or the --fix flag when one clears the finding. It is omitted when you have to make the change by hand.
The two lint captures above ran the forge v1.0.14 binary against a project whose pin was set to v1.0.0 with --allow-version-skew, so the rule’s introduced_in and gates_from shown are the ones the rollout table declares for that rule. They illustrate the mechanism for any rule in its grace period.

lint.rules wins, in both directions

lint.rules in forge.yaml always beats the rollout, because naming a rule is an explicit decision. The rollout only supplies the default for a rule you have not spoken about.
  • error opts in early. Do this once you have cleaned the findings, so a regression cannot slip in during the grace period.
  • warn extends the grace period for as long as you need, even past gates_from.
  • off drops the finding.
  • The golangci-lint pass over files behind your build tags is graded under the key golangci-build-tags. It is the one rule key that stands for another tool’s findings; "*" never reaches golangci-lint’s own linters.
A key that names no gradable rule is an error, so a typo cannot quietly disable a rule. See Silencing a rule.

Bumping the pin

When you move forge_version forward, run lint before you commit the bump. Anything that was in its grace period at the old pin and reaches gates_from at the new one will start to fail. The warnings you saw before the bump are the exact list. Clear them first (most have a mechanical fix: forge lint --fix, forge fix connect-errors, forge fix narrow-deps <package-dir>), or pin them to warn in lint.rules with a note that says when you will turn them back on. forge project upgrade list shows the one-time migration playbooks that apply to a pin bump. The rollout is a complement to them, not a replacement: the playbooks cover changes to files, the rollout covers new checks.

For forge contributors

Adding a rule to forge means adding it to gradableRules in lint_rules.go and a row to newRules in lint_rollout_table.go, with IntroducedIn set to the release the rule will ship in and GatesFrom to the one after. TestEveryGradableRuleDeclaresItsRollout fails until you do, and TestRolloutRowsAreWellFormed refuses a row whose GatesFrom equals its IntroducedIn. A rule that must gate on arrival needs the owner’s say, because it breaks every pinned consumer. Tightening an existing rule is the same act: move its ID from shippedRules to newRules and its findings warn again for one release.

Where to go next

Linting overview

The lanes, flags, the verdict line, and where lint runs.

Rule reference

Every rule with its rollout status in one table.