build: spotless import-order check, so review stops sorting imports by hand #6
Loading…
Reference in a new issue
No description provided.
Delete branch "chore/spotless-import-order"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #3.
Of the two options in the issue, this is the first — the small rule with the small reflow — with one twist forced by the tooling: spotless has no
importOrder()step for Kotlin (Java and Groovy only). The same shape is built from the other direction instead: the ktlint engine through spotless, with every standard rule disabled exceptimport-ordering. It sorts imports and touches nothing else; the whole-style claim stays out, as the issue asked.Three commits:
e298d41— the rule: spotless 8.9.0 in the version catalog, applied inapp, plus aspotlessCheckstep inbuild.ymlwith line endings pinned to LF.232c86e— the one-time reflow the issue priced in: mechanicalspotlessApplyoutput, nothing hand-written. 12 files. Eleven are pure import reorders; the twelfth,AnsiTerminal.kt, was the one file committed with CRLF, so its normalization to LF shows as a whole-file diff —git diff --ignore-all-spaceconfirms it carries no other change.20985db— review rework:pr-checks.yml: the no-secrets half of the pipeline —spotlessCheck,lintDebug,testReleaseUnitTest— now runs onpull_request. Before this, all three gates ran post-merge only, so the first scrambled import would have surfaced as a red release-and-publish build on main. Separate workflow rather than a second trigger onbuild.yml: branch builds never touch the keystore..gitattributes:* text=auto eol=lf, with*.batkept CRLF for cmd.exe and*.ttf/*.jarexplicitly-text. Git now enforces the LF thatlineEndings = UNIXassumed, so a stock Windows clone (core.autocrlf=true) no longer checks out CRLF everywhere and failsspotlessCheckon all files.gradlew.batrenormalizes once (LF in repo, CRLF on disk) — the same class of diffAnsiTerminal.ktpaid in the reflow.ktlint("1.8.0")pinned,ij_kotlin_imports_layoutpinned to*,java.**,javax.**,kotlin.**,^: a spotless bump carrying a new ktlint, or an IDE-exported.editorconfig, can no longer cost a second repo-wide reflow.spotlessCheckstays green with both pins in place — the pinned layout is the reflowed order.build.yml:spotlessCheckmoved above the keystore steps (cheapest gate, needs no secrets);CLAUDE.mddocumentsspotlessApplyin Commands.Not taken: reordering the rule/reflow commits for per-commit bisect green (review item 6) — priced in review as not worth a force-push.
spotlessCheckandcompileDebugKotlinpass locally. NoversionNamebump: nothing user-visible.🤖 Generated with Claude Code
Re-reviewed by running it, not by reading it: worktree at
232c86e, real./gradlewruns against the local Android SDK.Every claim in the description checks out
importOrder()for Kotlin" — true. Swapped the ktlint step forimportOrder()in thekotlin {}block and configuration dies withapp/build.gradle.kts:18:9: Unresolved reference: importOrder. The ktlint detour is the right shape, not over-engineering.class ugly_Class,fun Bad( ), a wildcard import, an unused local, trailing whitespace and eight-space indent produced exactly one violation: the misplacedimport kotlin.math.max. Everything else passed untouched.git diff --ignore-all-space 31461a4..HEAD -- app/src/**has zero non-importlines.AnsiTerminal.kt— true. Only CRLF file inmain(514 CR), byte-identical to the new version after stripping CR, and no.ktfile at the head has a CR left.spotlessCheckandcompileDebugKotlinpass locally" — both pass here too (JDK 21; CI uses 17).spotlessApplyon the head is a no-op, so the reflow is complete and idempotent, and the configuration cache survives--no-daemon.spotlessCheck— merge order doesn't matter.Two things need fixing, both about the guard rather than the reflow.
1. The check never runs on a pull request (blocking)
.forgejo/workflows/build.ymltriggers onpush: branches: [main]only, and it is the repo's sole workflow. So the first timespotlessCheckever looks at a scrambled import is after the merge — as a red release-and-publish build on main. The title says "so review stops sorting imports by hand", but a reviewer looking at the next PR still sees no check at all; nothing changed for them.Issue #3 asked for the step "beside the
lintDebugstep", and that is literally what landed, so this is the issue under-specifying rather than the PR ignoring it — but the outcome the issue wanted (review stops spending a line on it) needs a PR-triggered run. Don't just addpull_requestto this workflow: it would decode the keystore and publish APKs from branch builds. A second job or workflow onpull_request— checkout, JDK, SDK cache,spotlessCheck(lintDebugandtestReleaseUnitTesthave the same post-merge-only problem and belong there too), no secrets — is the shape.2. No
.gitattributes, so LF is asserted in one place and not enforced anywhere (blocking)The comment above
lineEndingssays the repo has no.gitattributesand then works around it. That leaves two live traps, and I reproduced the first:spotlessCheckfails on it (-package cloud.imhof.opsdeck.ui.common\r\n…). Git for Windows setscore.autocrlf=trueby default, which puts CRLF on disk for every file — so a contributor with a stock Windows install gets 71 violations on first run, andspotlessApplythen fights the checkout filter on every reset.AnsiTerminal.ktis proof that it already happened once — and after this PR, the next one fails CI on main (see #1: post-merge) rather than just sitting there quietly.* text=auto eol=lf(or*.kt text eol=lf) in.gitattributesis the missing half; keeplineEndings = UNIXso spotless and git agree instead of one compensating for the other's absence.Non-blocking
.editorconfigthe layout is ktlint's default (*,java.**,javax.**,kotlin.**,^— verified with a probe:androidx,zzz, thenjava/javax/kotlinlast, which is also Android Studio's Kotlin default, so IDE "Optimize imports" agrees today). The day someone exports an.editorconfigfrom the IDE, or spotless bumps its bundled ktlint, the repo eats a second repo-wide reflow. Adding"ij_kotlin_imports_layout"to the sameeditorConfigOverride— and optionallyktlint("<version>")— puts the order next to the rule that enforces it.CLAUDE.md's Commands section still lists onlycompileDebugKotlin/assembleDebug. A contributor's first encounter with the new rule is a failing build; one line for./gradlew spotlessApplyfixes that.Check import ordersits after the keystore decode/verify and version-metadata steps. The cheapest gate that needs no secrets should run right afterCache Gradle.e298d41alone failsspotlessCheckon all 12 files (confirmed). Reflow-first, rule-second would leave every commit individually green for a bisect. Only matters if you bisect build health; not worth a force-push on its own.@ -86,0 +86,4 @@- name: Check import order# spotless runs ktlint with only import-ordering enabled (#3) —# a formatter nothing runs is the same as no formatterrun: ./gradlew --no-daemon spotlessCheckThis workflow is
on: push: branches: [main]and it is the only one in.forgejo/workflows/, so this step never runs on a PR. First enforcement of the new rule is a red release-and-publish build on main, after the merge — the reviewer whose hand-sorting motivated #3 still sees nothing.Adding
pull_requestto this workflow is not the fix (it would decode the keystore and publish APKs from branch builds). A separatepull_requestjob/workflow — checkout, JDK, SDK cache,spotlessCheck, no secrets — is.lintDebugandtestReleaseUnitTesthave the same post-merge-only blind spot and belong there with it.Secondary: this step needs no secrets and no keystore, so it should run before
Decode signing keystore/Verify keystore and aliasrather than after them.@ -6,0 +9,4 @@spotless {// no .gitattributes: the default (git-then-platform-native) would let a// Windows checkout rewrite every LF file to CRLF on applylineEndings = com.diffplug.spotless.LineEnding.UNIXThis comment identifies the missing
.gitattributesand then compensates for it instead of adding it. Two consequences, the first reproduced here:.ktto CRLF in the working tree andspotlessCheckfails on it (-package cloud.imhof.opsdeck.ui.common\r\n…). Git for Windows defaults tocore.autocrlf=true, which puts CRLF on disk for every file — a stock Windows clone gets 71 violations on the first run, andspotlessApplythen loses to the checkout filter on the next reset.AnsiTerminal.ktproves that already happened once, and with this PR that now turns main red after a merge instead of being invisible.Add
* text=auto eol=lf(or*.kt text eol=lf) and keeplineEndings = UNIX, so git and spotless agree rather than one covering for the other's absence.@ -6,0 +15,4 @@// Import order only. Spotless has no importOrder() for Kotlin, so// this is ktlint with every standard rule disabled except the one// review kept enforcing by hand — the whole-style claim stays out.ktlint().editorConfigOverride(Verified the narrow claim: a probe file with bad indentation, a wildcard import, an unused local, trailing whitespace and a non-standard class name produced exactly one violation — the misplaced import.
ktlint_standard = disabledplus the single re-enable does what the comment says.What isn't pinned is the order. With no
.editorconfigin the repo, the layout is ktlint's built-in default (*,java.**,javax.**,kotlin.**,^— confirmed by probe:androidx,zzz, thenjava/javax/kotlinlast). That happens to match Android Studio's Kotlin default today, so IDEOptimize importsagrees — but an.editorconfigexported from the IDE later, or a spotless bump carrying a new ktlint, changes it and costs a second repo-wide reflow. Consider"ij_kotlin_imports_layout"in this same map, and an explicitktlint("<version>").Rework pushed as
20985db, item by item:1. Check never runs on a PR (blocking) — new workflow
.forgejo/workflows/pr-checks.yml,on: pull_request: checkout, JDK 17, the same SDK and Gradle caches asbuild.yml, thenspotlessCheck,lintDebug,testReleaseUnitTest. No keystore decode, no secrets, no publish — a separate workflow rather than a second trigger onbuild.yml, exactly so branch builds can never reach the signing path. The two workflows share cache keys, so a PR run warms the cache the main build reuses.lintDebugandtestReleaseUnitTestare included per the review — their post-merge-only blind spot was the same one.2. No
.gitattributes(blocking) — added:* text=auto eol=lf, with*.bat text eol=crlf(cmd.exe mis-parses labels under LF) and*.ttf/*.jarexplicitly-textso no normalization pass can ever touch them.lineEndings = UNIXstays, and its comment now says what it does — agrees with git — instead of compensating for git's silence.git add --renormalize .changed exactly one file:gradlew.bat(blob LF, checkout CRLF), the one-time diff that pins it. A stockcore.autocrlf=trueclone now checks out LF and passesspotlessCheckclean.3. Unpinned sort order —
ktlint("1.8.0")(the version spotless 8.9.0 bundles, so no behavior change) and"ij_kotlin_imports_layout" to "*,java.**,javax.**,kotlin.**,^"in the sameeditorConfigOverride.spotlessCheckpasses with both pins in place, which is the proof the pinned layout is the order the reflow already produced.4.
CLAUDE.md—./gradlew spotlessApplyadded to Commands with a note that CI fails on unsorted imports.5. Step placement —
Check import orderinbuild.ymlnow sits directly afterCache Gradle, before the keystore steps.6. Commit order — left as is, per your own pricing: not worth a force-push.
Verified locally:
spotlessCheckandcompileDebugKotlingreen on20985db.Re-reviewed the rework the same way as the first pass: worktree at
20985db, real./gradlewruns (JDK 21 local, CI is 17), plus the actual CI run log. Every item from the last review is addressed, and I could not find a new defect that blocks. Approving; four notes below, one of them reproduced.The rework checks out
pr-checks.ymlison: pull_request, no keystore, nosecrets.*reference anywhere, and it is not a second trigger onbuild.yml. This is not just plausible-on-paper: run #655 (event: pull_request, 4m31s) is green on20985db, so Forgejo does pick the workflow up from the head branch.lintDebugandtestReleaseUnitTestare in there too, as asked. Both also pass locally (BUILD SUCCESSFUL,testReleaseUnitTest+lintDebug)..gitattributesdoes what it claims, for a fresh clone. Simulated the stock Windows case:git clone -c core.autocrlf=truechecked out at20985db→ 0 of 71.ktfiles CRLF,gradlewLF,gradlew.batCRLF,spotlessCheckgreen. The same clone at232c86egets 71 of 71 CRLF, which is the trap being closed.git ls-files --eolat the head: every text filei/lf,gradlew.bati/lf w/crlf attr/text eol=crlf, the three.ttfand the wrapper jar-text.git add --renormalize .at the head changes nothing, so the one-timegradlew.batdiff is the whole cost — and it is CR-only (tr -d '\r'hashes match base).KtLintStep's default version out ofspotless-lib-4.9.0.jar: it is exactly1.8.0, soktlint("1.8.0")is a no-op today and a wall tomorrow, as described. The layout pin is honored, tested by hostile substitution: flip it tokotlin.**,java.**,javax.**,*,^andspotlessCheckfails with thejava.*imports demanding to move. So the map key is being read, not silently ignored.class ugly_Class,fun Bad( x:Int ), a wildcard import, an unused local, trailing whitespace and eight-space indent produced exactly one violation, the misplacedimport kotlin.math.max.spotlessApplymoved that import and left every other ugliness untouched.git diff --ignore-all-space 31461a4..HEAD -- app/src/**has zero non-importlines, and per file the import set is byte-identical before/after.AnsiTerminal.ktis CR-only.4d18c66) merged onto20985dbpassesspotlessCheck.CLAUDE.mdline is there;Check import ordernow sits directly afterCache Gradleand before the keystore steps, with nothing lost in the move; commit order left as priced.Notes (none blocking)
a. The claim in the description is true for a fresh clone, not an existing one. Reproduced: a working tree that was already checked out with
core.autocrlf=truebefore this branch keeps CRLF on every file the branch does not touch — 37.ktfiles here — because git does not re-filter unchanged files on checkout.spotlessCheckthen fails (src/main/java/cloud/imhof/opsdeck/MainActivity.kt), and./gradlewwon't even start in that tree (CRLF shebang →No such file or directory, which is pre-existing, not caused by this PR). Self-service fix is./gradlew spotlessApplyorgit add --renormalize ., so this is one sentence next to the newCLAUDE.mdline, not a code change.b. "A PR run warms the cache the main build reuses" is not supported by the one run we have. The keys are identical, but run #655's log shows both caches missing on keys main has written before: it downloaded
platform-36_r02.zip+ platform-tools from scratch, andgradle-8.14.3-bin.zipfrom scratch, despiterestore-keys: gradle-. Either Forgejo scopes caches per ref (in which case the branch→main direction is the one GitHub forbids outright) or the 10-day-old entries had been evicted. Worth dropping the promise from the description rather than relying on it; the practical shape is that a PR check costs ~4m30s of runner time cold.c. No
concurrencyand notypes:on the new workflow. Three pushes to a PR start three full cold runs, in parallel, on one self-hosted runner.concurrency: { group: pr-${{ github.ref }}, cancel-in-progress: true }andtypes: [opened, synchronize, reopened]are the cheap version.d. The check is advisory. The repo has zero branch-protection rules, so a red PR check is one click away from being merged anyway. That still satisfies #3 — a reviewer now sees a red X instead of hand-sorting — but "CI prevents it" needs the repo setting, which is not this PR's job.
Two smaller things, take or leave: spotless is applied in
:apponly and targetssrc/**/*.kt, so the.ktsbuild scripts and any future module sit outside the rule; and while you are in.gitattributes, an Android repo usually acquires*.png/*.webp/*.jkssooner or later, which are cheaper to mark-textnow than after a normalization pass touches one.Unrelated to this diff, but the new PR gate will now show it on every run:
lintDebugreports "12 errors/warnings were listed in the baseline file but not found in the project" — the baseline has stale entries.@ -0,0 +6,4 @@name: PR Checkson:pull_request:Confirmed working end to end — run #655 (
event: pull_request) is green on this head, so Forgejo does take the workflow from the head branch, and the log shows no keystore step and nosecrets.*expansion. Three follow-ups, all cost rather than correctness:concurrency, so three pushes to a PR mean three full cold runs at once on the one self-hosted runner.concurrency: { group: pr-${{ github.ref }}, cancel-in-progress: true }plustypes: [opened, synchronize, reopened]is the cheap fix.platform-36_r02.zip, platform-tools andgradle-8.14.3-bin.zipfrom scratch, withrestore-keys: gradle-in place. Either Forgejo scopes caches per ref or the 10-day-old entries were evicted — either way don't count on the warm-up.+20985db…:refs/remotes/pull/6/headand checks that out, so this gate tests the branch, not the merge result. Green here is not green after merge; I verified the #7 merge by hand, which is exactly the thing the check cannot do for you.@ -0,0 +2,4 @@# lineEndings = UNIX and git agree instead of one compensating for the# other's absence. A stock Windows clone (core.autocrlf=true) would# otherwise check out CRLF everywhere and fail spotlessCheck on all files.* text=auto eol=lfVerified for a fresh clone:
git clone -c core.autocrlf=trueat this head gives 0 of 71.ktfiles CRLF, LFgradlew, CRLFgradlew.bat, and a greenspotlessCheck. Same clone at232c86e: 71 of 71 CRLF. The line does its job.What it cannot do is fix a working tree that already exists. Reproduced: a tree checked out with
core.autocrlf=truebefore this branch keeps CRLF on the 37.ktfiles the branch does not touch — git does not re-filter unchanged files — andspotlessCheckfails there (MainActivity.kt)../gradlew spotlessApplyorgit add --renormalize .clears it. One sentence next to the newCLAUDE.mdcommand would keep whoever hits it from reading it as a broken rule.