Skip to content

feat: add flint v2 Rust binary#139

Merged
zeitlinger merged 142 commits intomainfrom
feat/flint-v2
Apr 10, 2026
Merged

feat: add flint v2 Rust binary#139
zeitlinger merged 142 commits intomainfrom
feat/flint-v2

Conversation

@zeitlinger
Copy link
Copy Markdown
Member

@zeitlinger zeitlinger commented Apr 2, 2026

Summary

Introduces the flint Rust binary: a mise-native lint orchestrator that replaces the v1 bash task scripts.

How it works: reads installed tools from mise.toml, maps them to a built-in check registry, runs checks against changed files (merge-base diff) in parallel. Special checks (lychee links, renovate-deps) are implemented in Rust.

File breakdown

282 files changed — the count is misleading without context:

Category Files
tests/ — e2e test fixtures (70 test cases × ~3-4 files each) 248
src/ — Rust source 13
Config, docs, CI, Cargo 21

The 23 linters covered by e2e tests: actionlint, biome, biome-format, cargo-clippy, cargo-fmt, codespell, dotnet-format, editorconfig-checker, gofmt, golangci-lint, google-java-format, hadolint, ktlint, license-header, lychee, markdownlint-cli2, prettier, renovate-deps, ruff, ruff-format, shellcheck, shfmt, plus general cases.

What's not in this PR

Deferred features:

  • flint hook install — installs a git pre-commit hook; replaces the per-repo mise run setup:pre-commit-hook task with something self-contained in the CLI
  • check_task / fix_task in flint.toml — allows a fast script (e.g. regex/Python) as the check with a slow canonical fixer (e.g. Gradle); motivating use case: javaagent's StaticImportFormatter
  • Biome config injection — --config-path takes a directory, not a file; needs a directory-injection variant in the registry API

Low-priority linters (no consuming repo needs them yet):

  • merge-conflict-markers — pure-Rust special check
  • dotenv-linter, go-mod-tidy, xmllint

Post-merge:

  • Bash task scripts (tasks/lint/) — retire once consumer PRs are merged
  • GitHub release / github:grafana/flint registration — cut after this PR merges; consumer repos switch from branch tracking to a pinned version

Consumer migration status

All 7 consumer PRs green — validated across Rust, Go, Java, Kotlin, Python, .NET, Shell, Dockerfile:

  • grafana/mox #63
  • grafana/oats #272
  • grafana/otel-checker #267
  • grafana/grafana-opentelemetry-java #1251
  • grafana/docker-otel-lgtm #1243
  • prometheus/client_java #1988
  • open-telemetry/opentelemetry-java-instrumentation #17759

Test plan

  • CI passes
  • flint list shows all registry entries with correct installed/missing status
  • All 7 consumer PRs pass CI

Release-As: 0.20.0

Working binary that discovers tools from PATH via built-in registry,
runs them in parallel against changed files (merge-base diff), and exits
non-zero on failure. Replaces run-linters.sh for standard linters.

Supported linters: shellcheck, shfmt, markdownlint, prettier, actionlint,
hadolint, codespell, ec, golangci-lint, ruff, ruff-format, biome,
biome-format.

Flags: --fix (AUTOFIX env), --full, --from-ref, --to-ref, [linters...]
Config: optional flint.toml with settings.base_branch and settings.exclude.
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
- Add CheckKind enum to registry to support special checks alongside
  template-based ones; restructure Check with explicit bin_name field
- Add links check: lychee orchestration with diff-mode/full-mode logic,
  config-change fallback, check_all_local opt-in, and GitHub remap args
  (same-repo PR → local file paths; fork PR → raw.githubusercontent.com)
- Add renovate-deps check: embeds renovate-deps.py via include_str! and
  runs it as a subprocess; tagged slow=true, supports --fix via AUTOFIX
- Add ChecksConfig / LinksConfig / RenovateDepsConfig to config.rs
- Parallel runner now captures and defers output; fix mode is serial

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
- Add textlint, cargo-clippy, cargo-fmt to built-in registry
- Fix Project-scope checks to respect patterns: skip when no matching
  files present (prevents cargo-clippy running in non-Rust repos)
- Add flint.toml for self-config (links, renovate-deps, excludes)
- Wire mise lint/fix/native-lint tasks to build-then-run local binary
- Add linting tools to [tools] so mise installs them for CI
- Remove bash-backed lint tasks (scripts stay until M5)
- Fix clippy and fmt issues caught by dogfooding

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
- Drop npm:textlint and npm:textlint-rule-terminology (mise npm isolation
  incompatible with textlint plugin architecture; see design doc)
- Drop SUPER_LINTER_VERSION and setup:native-lint-tools (superseded by
  flint binary in M4)
- Remove textlint from built-in registry
- Add README link to FLINT-V2.md
- Regenerate renovate-tracked-deps.json

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Suppresses per-check output and prints only the summary line:
  flint: 2 checks failed (shellcheck, prettier)

Enabled via --short or FLINT_SHORT=true. Native-lint task now
uses --short by default since it is the target called by the
CC pre-push hook.

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
In --short mode, partition failed checks by fixability and emit the
exact command for fixable ones: `flint --fix prettier shfmt` instead
of `fix: prettier, shfmt`. AI callers can execute the command directly
without a reasoning step. Non-fixable checks remain under `review:`.

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
ec (editorconfig-checker) was running on all files including *.rs, *.py,
*.go, *.sh, *.json etc. — types that already have dedicated formatters.
Add exclude_patterns to the Check registry and set it on ec to defer to
those formatters for the types they own.

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Static exclude_patterns always skipped e.g. *.rs from ec even when
cargo-fmt wasn't installed. Replace with excludes_if_active: a list of
check names that, when active, cause their patterns to be excluded from
ec's file list. ec falls back to checking those files when the dedicated
formatter isn't present.

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
shfmt, golangci-lint, and markdownlint don't enforce line length so
they don't conflict with ec's max_line_length check. Only exclude from
ec when cargo-fmt, ruff-format, biome-format, or prettier are active.

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
--auto fixes all fixable checks, reports outcome, and exits 0 only if
everything passed or was fixed. Non-fixable failures (and failed fix
attempts) are surfaced under 'review:'. Intended for pre-push hooks and
agentic pipelines that have write access.

  flint: fixed: prettier cargo-fmt | review: shellcheck

Update native-lint to use --auto --fast so the CC hook auto-fixes
rather than round-tripping through the agent.

Docs updated with intended-use-by-context table.

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
Fixes a bug where --auto exiting 0 after fixing would let the push
proceed with unfixed commits. Now exits 1 with 'commit before pushing'
whenever fixes were applied, so the agent knows to stage and commit
the changes first. Only exits 0 if everything was already clean.

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
@zeitlinger zeitlinger marked this pull request as ready for review April 2, 2026 18:34
@zeitlinger zeitlinger requested a review from a team as a code owner April 2, 2026 18:34
Copilot AI review requested due to automatic review settings April 2, 2026 18:34
@zeitlinger zeitlinger changed the title feat: add flint Rust binary (M1–M4) feat: add flint v2 Rust binary (M1–M4) Apr 2, 2026
@zeitlinger zeitlinger changed the title feat: add flint v2 Rust binary (M1–M4) feat: add flint v2 Rust binary Apr 2, 2026
… differs

After replacing install_key with mise_tool, check_active started looking
for npm:/pipx:/github: prefixed keys only. Repos (and e2e fixtures) that
declare the tool under the bare name (markdownlint-cli2, biome, ruff, etc.)
stopped matching. Accept bin_name as a fallback when mise_tool_name is set.
…an/sh

Update shfmt e2e test fixtures to declare 'github:mvdan/sh' = 'v3.12.0'
so resolve_bin_name returns the correct versioned binary name (shfmt_v3.12.0)
that ubi installs.

Also restore check_active fallback: when mise_tool_name differs from
bin_name, also accept the bare bin_name key so repos declaring the tool
under a bare name (markdownlint-cli2, biome, ruff etc.) continue to work.
Prevents one flaky platform (e.g. renovate-deps on macOS) from
cancelling the other matrix legs before they complete.
canonicalize() on Windows returns \\?\C:\... verbatim paths.
Git and other tools don't handle UNC CWDs correctly, causing git ls-files
to return nothing and all e2e checks to silently skip. Strip the prefix
back to a regular drive path while keeping the macOS /private fix.
On Windows, mise creates .cmd shim files that cannot be spawned directly
via CreateProcessW — they require cmd.exe as the interpreter. Using
cmd.exe /C handles both .cmd shims (npm tools: prettier, biome, etc.) and
the versioned shfmt_v3.12.0.cmd shim. Applies only on Windows; Unix paths
are unchanged.

Also remove the temporary debug step from test.yml.
…empty result

Windows e2e: strip \\?\ from canonical path before substitution (so
tool output using long names matches <REPO> when repo.path() has 8.3 short
names), then normalize backslashes to forward slashes so Unix snapshots
match Windows tool output.

macOS flakiness: renovate occasionally produces empty packageFiles on first
run due to transient network issues. Retry up to 3 times with a 3s delay
before accepting an empty result.
… normalize

- Strip \r\n/\r so Windows tool output (CRLF) matches Unix snapshots
- Normalize \ to / in both the output AND the repo path strings before
  substitution, so both shfmt (backslash paths) and lychee (file:// URIs
  with forward slashes) get <REPO> substituted correctly
… normalization

- cmd.exe /C wrapping for renovate and lychee (same fix as runner.rs)
- Strip //?/ UNC prefix that leaks through after backslash normalization
  (e.g. cargo-fmt outputs Diff in //?/C:/... → strip to Diff in C:/...)
- Collapse file:///<REPO> → file://<REPO> to match Unix snapshots
- Extract spawn_command() into linters/mod.rs — single place for the
  cmd.exe /C wrapping; all three callers (runner, lychee, renovate_deps) now
  use it instead of duplicating the cfg(windows) block.
- Add dunce crate; replace manual \\?\ stripping in main.rs and e2e.rs
  with dunce::canonicalize() which handles both macOS /private/ symlinks
  and Windows verbatim paths in one call.
- Extract canonical_repo_path() and normalize_output() helpers in e2e.rs
  so all Windows path normalization (CRLF, \→/, //?/, file:///) is in one
  place.
…tnet fixtures

PE detection: some tools (ktlint) are native PE binaries without .exe
extension. cmd.exe can't resolve them and the mise shim also fails.
Detect MZ magic bytes and execute such binaries directly by full path,
falling back to cmd.exe /C for .cmd shims.

dotnet-format fixtures: add .editorconfig with end_of_line = lf so
dotnet uses LF on Windows, matching the Unix test snapshots.
spawn_command: detect self-executing JARs (#!/ magic + >1MB) and invoke
via 'java -jar' instead of cmd.exe. Handles ktlint which ships as a
Unix self-executing JAR — cmd.exe can't run it and the mise shim fails.

dotnet-format fixtures: fix .editorconfig syntax (root=true, [*.cs]
section) and add end_of_line=lf to prevent dotnet on Windows from
suggesting CRLF-based whitespace fixes.
…et output

Replace global \→/ with a regex that only normalizes backslashes flanked
by path-component characters (alphanumeric, ., /, >, -). This preserves
dotnet's \s whitespace notation in diagnostic strings like Insert '\s\s\s\s'
while still normalizing actual path separators like <REPO>\script.sh.orig.
Replaces the flanking-char regex with a simpler approach: match the entire
<REPO>path... sequence and replace all \ within it. This correctly handles
multi-level paths (e.g. <REPO>\src\lib.rs) while leaving dotnet's \s\s\s\s
whitespace notation untouched since it never appears inside a <REPO>-prefixed
path.
Replace <REPO>-scoped regex with a character-level walk that converts
every \ to / except when inside single quotes. This handles:
- Relative paths: .github\workflows → .github/workflows (actionlint, renovate)
- Cargo/rustc paths: --> src\lib.rs → --> src/lib.rs
- Lychee: [.\README.md] → [./README.md]
And correctly preserves:
- dotnet whitespace notation: Insert '\s\s\s\s' (inside single quotes)
@zeitlinger
Copy link
Copy Markdown
Member Author

@martincostello finally passing with all platforms

@zeitlinger zeitlinger merged commit 19f2b25 into main Apr 10, 2026
13 checks passed
@zeitlinger zeitlinger deleted the feat/flint-v2 branch April 10, 2026 18:06
zeitlinger pushed a commit that referenced this pull request Apr 13, 2026
## flint v0.20.0 — Rust rewrite

flint has been rewritten from scratch in Rust. The bash + Docker stack
is replaced by a fast, cross-platform native binary with no container
dependency.

### Highlights

- **Native binary** — no Docker, no bash wrappers; runs anywhere mise
can install a Rust crate
- **Parallel linting** — all checks run concurrently
- **Diff-aware** — only checks files changed since the merge base by
default (`--full` for everything)
- **`flint update`** — migrates obsolete `mise.toml` tool keys
automatically (e.g. `ubi:` → `github:`, `npm:markdownlint-cli` →
`npm:markdownlint-cli2`)
- **`flint hook install`** — installs a pre-commit hook without any mise
task knowledge
- **New linters**: `gofmt`, `google-java-format`, `ktlint`,
`dotnet-format`, `markdownlint-cli2`, `xmllint` (via `cargo:xmloxide`),
`license-header` (pure-Rust, no binary)
- **Windows support** — ktlint self-executing JAR handled via explicit
`java -jar` invocation

### Migration

See
[AGENTS-V2.md](https://github.com/grafana/flint/blob/main/AGENTS-V2.md)
and [README.md](https://github.com/grafana/flint/blob/main/README.md)
for full setup instructions.

Quick reference for existing consumers:

```toml
[tools]
# While installing from source (pre-crates.io release):
"cargo:https://github.com/grafana/flint" = "branch:main"

[env]
FLINT_CONFIG_DIR = ".github/config"

[tasks.lint]
run = "flint run"

[tasks."lint:fix"]
run = "flint run --fix"
```

Remove: `lint:super-linter`, `lint:links`, `lint:renovate-deps`,
`setup:native-lint-tools`, `setup:pre-commit-hook` tasks. Run `flint
update` to auto-migrate any obsolete tool keys.

> [!NOTE]
> The changelog below includes entries from the legacy v1 bash era — see
[README-V1.md](https://github.com/grafana/flint/blob/main/README-V1.md)
for v1 docs.

---

##
[0.20.0](flint-v0.19.0...flint-v0.20.0)
(2026-04-13)


### Features

* add flint v2 Rust binary
([#139](#139))
([19f2b25](19f2b25))
* add native linting mode and version mapping infrastructure
([#93](#93))
([24b06da](24b06da))
* add Renovate shareable preset for consuming repos
([#17](#17))
([8a06590](8a06590))
* consolidate link checking and add autofix flags
([#7](#7))
([086a5e9](086a5e9))
* flint update command, explicit JAR flag, v0.20.0
([#146](#146))
([b43bf52](b43bf52))
* handle line-number anchors and issue comments globally
([#56](#56))
([cf751df](cf751df))
* **links:** add GitHub URL remaps for line-number and fragment anchors
([#28](#28))
([5b59065](5b59065))
* **links:** auto-remap base-branch GitHub URLs to PR branch
([#18](#18))
([dd6cc61](dd6cc61))
* **renovate:** support SHA-pinned URLs in Renovate preset
([#21](#21))
([4fd1f28](4fd1f28))
* **super-linter:** default to slim image
([#24](#24))
([c8eeab8](c8eeab8))
* support NATIVE env var for container-free linting
([#107](#107))
([0a8193d](0a8193d))


### Bug Fixes

* activate mise environment in native lint mode
([#123](#123))
([d0fec45](d0fec45))
* add 'mise run fix' hint to lint failure output
([#90](#90))
([5b4ad5d](5b4ad5d))
* decouple version mapping generation from pinned super-linter version
([#112](#112))
([5370e77](5370e77))
* **deps:** update rust crate crossterm to 0.29
([#156](#156))
([c59ae3e](c59ae3e))
* **deps:** update rust crate similar to v3
([#160](#160))
([684be4e](684be4e))
* **deps:** update rust crate toml to v1
([#161](#161))
([3aae614](3aae614))
* **deps:** update rust crate toml_edit to 0.25
([#158](#158))
([42d9efd](42d9efd))
* exclude GitHub compare links from lychee checks
([#10](#10))
([e714608](e714608))
* fail native lint when enabled tools are missing
([#111](#111))
([163bb6b](163bb6b))
* improve link checker reliability against GitHub rate limiting
([#95](#95))
([7a5282d](7a5282d))
* include staged files in native lint file list
([#135](#135))
([34412d6](34412d6))
* **links:** add regex anchors to remap patterns
([#19](#19))
([2e17348](2e17348))
* native lint in worktrees, trust toml, use ec binary, drop isort
([#134](#134))
([8594bba](8594bba))
* **release-please:** fix footer not appearing on release PRs
([#40](#40))
([d7a55e4](d7a55e4))
* remap same-repo GitHub URLs to local file paths
([#100](#100))
([b4feadd](b4feadd))
* **renovate-deps:** forward GITHUB_TOKEN as GITHUB_COM_TOKEN
([#132](#132))
([4d6510b](4d6510b))
* replace broken release-please PR comment with docs
([#12](#12))
([817b37d](817b37d))
* run shellcheck on .bats files in native mode
([#137](#137))
([a4fd3f8](a4fd3f8))
* strip Scroll to Text Fragment anchors in link checks
([#86](#86))
([b630cdf](b630cdf))
* tighten markdownlint config for native mode
([#106](#106))
([6ef25b2](6ef25b2))
* use remap instead of exclude for issue comment anchors
([#58](#58))
([656f355](656f355))

---
> [!IMPORTANT]
> Close and reopen this PR to trigger CI checks.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This was referenced Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants