build: the five actionable notes from the #6 review #9
Loading…
Reference in a new issue
No description provided.
Delete branch "chore/pr-check-cost-and-spotless-scope"
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 #8 items 1–5. Nothing here is a fix to #6 — that landed correct. These are the notes the approving review left beside it, all cost, scope or noise rather than correctness.
Four commits, one per note (items 2 and 3 share one because they are the same file pair), plus five rework commits from three review rounds.
dde4f70is a rewordeda8d1df2— force-pushed, tree byte-identical, see the last section for why.59ae565+b8a4808+2e96246+ba496e5—concurrencyandtypes:on the PR gate. Neither changes what runs today, and the first version of this PR claimed otherwise.types: [opened, synchronize, reopened]is exactly the platform default; pinning it only guards against a future default change widening the trigger. What the explicitconcurrencyblock certainly adds is a named per-PR group (github.refisrefs/pull/<n>/headunderpull_requeston this instance). The comment has now been through four passes on one sentence — it says Forgejo cancels a superseded push run with no group declared, cites #295/#296 and #323/#324 as the evidence and labels both asevent: pushon main, and marks both remaining halves —synchronize, andopened/reopened— as assumed on the same unmeasured evidence. Every cancelled run this repo has ever had is a push run — all four — and nopull_requestrun here has ever been superseded, so nothing measures either.f71b16d+a53bb8a+5da20e0+ba496e5— the CRLF paragraph, and the-texttypes not tracked yet..gitattributesfixes a fresh clone; it cannot fix a tree that already exists, because git does not re-filter unchanged files. The paragraph exists so the next person does not read the rule as broken. It has taken four passes to get right and the current one is reproduced end to end — see Verification. It warns before the command instead of wrapping it ingit stash, it names the wrapper as a thing not to re-add, and it now says which clone this is even about: one made withcore.autocrlf=true. On a Linux or macOS default the pre-.gitattributescheckout was already LF, so without that clause a reader there could follow the paragraph into the one command in this repo that discards uncommitted work while chasing a problem they cannot have. Separately,-textgrows to png/webp/jpg/jpeg/jks/keystore — none tracked today, so nothing gets renormalized by adding them.c456369— spotless moves from:appto the root. It was configured insideapp/build.gradle.ktstargetingsrc/**/*.kt, so the rule stopped at one module's sources: build scripts exempt, and a second module exempt by default. Nowallprojectsat the root with the same pinnedktlint("1.8.0")and the same pinned layout, plus akotlinGradletarget relative to each project dir. Honest scope: today thekotlinGradletarget guards two import lines in one file;settings.gradle.ktsandapp/build.gradle.ktshave no imports at all. The LF check and any module added later are what it is for.dde4f70— twelve lint baseline entries that CI can never match. Not "dead" — that was wrong, and it was the subject line, which is why the commit got reworded rather than corrected in a follow-up. See below.Verification
Real
./gradlewruns against the local Android SDK (JDK 21 here, CI is 17).spotlessCheck,lintDebugandtestReleaseUnitTestall pass. Run #679 was green on2e96246; the two commits since touch one Markdown paragraph and one YAML comment block, and the workflow still parses to the sameon:andconcurrency:values it did onfc4c762— checked by parsing the file, not by reading it.The CRLF recipe is reproduced, wrapper and all. git 2.55.
git clone -c core.autocrlf=true --no-checkout, checkout20985db^(before.gitattributes), then checkout this head: 91 files landw/crlf,gradlew's shebang is#!/bin/sh^M, andgit status --porcelainreturns zero paths — thetext=auto eol=lfclean filter normalizes CRLF to LF before comparing. That is the whole trap, and it is a stronger reason to keep the paragraph than the one it replaced.From there, each line of the old recipe:
git stash→No local changes to save, no entry created.git rm --cached -r . && git reset --hard→ fixes it: 97w/lf, 1w/crlf(the.bat, as.gitattributesintends),gradlewshebang#!/bin/sh.git stash pop→No stash entries found., exit 1. With an unrelated entry on the stack, it instead applies and drops it — planted one, watched it land in the working tree and vanish fromgit stash list.And the claim that used to justify the paragraph:
git add --renormalize .leaves 91 files CRLF before and after, with zero status paths before and after. There was never anMfor it to clear.The
.ktshalf is enforced, not decoration. Hostile test: swap the two imports at the top of the root script and:spotlessKotlinGradleCheckfails with exactly that reorder —Four spotless task pairs now run where
mainruns one (:app:spotlessKotlin{,Check})::spotlessKotlin{,Check},:spotlessKotlinGradle{,Check},:app:spotlessKotlin{,Check},:app:spotlessKotlinGradle{,Check}. No reflow — every tracked file already satisfied the rule, so the diff carries no.ktor.ktscontent change.The Gradle 9 deprecation warnings are not mine.
spotlessCheck --warning-mode all --no-configuration-cacheprints the same threeis--property warnings before and after; all three come from AGP (isCrunchPngs,isUseProguard,isWearAppUnbundled).--no-configuration-cachematters: a stored config-cache entry suppresses them and the head looks like it has zero.The twelve lint baseline entries are a no-op in CI and noisier locally — not a no-op everywhere. The earlier claim ("an absolute path into another machine's checkout", "changes nothing except the noise") was wrong on both halves, and review was right. Their
<location file=...>is$HOME/Projects/git.imhof.cloud/OpsDeck/mobile/…— a$HOME-relative path that lint expands at read time, not a fixed absolute one. Reproduced by puttingmain's 17-entry baseline into a worktree at exactly that path:main(17)main(17)$HOME/Projects/git.imhof.cloud/OpsDeck/mobileSo 8 of the 12 do match where the path resolves, and the other 4 are dead because their version strings moved on. Removing them changes nothing in CI, and un-silences 8 dependency-freshness warnings in that one checkout. No gate risk either way: all 8 are warnings,
abortOnError = true, nowarningsAsErrors.Removed rather than regenerated on purpose:
updateLintBaselineDebugwrites 18 entries, not 5. It would swallow the 13 warnings CI currently shows, including aModifierParameterfinding nobody has triaged. The five kept entries use project-relativesrc/main/…paths, match everywhere, and are byte-identical.The two nits from the approval, taken
Both were explicitly "take it whenever, not now"; both are one clause, and both are in
ba496e5.pr-checks.yml— "extends cancel-in-progress to opened and reopened, which the implicit default does not cover" was the last flat assertion about implicit behaviour left in that comment, and it rests on exactly the evidence thesynchronizehalf two lines above already calls assumed: noopenedorreopenedrun in this repo has ever been superseded either. It now says so rather than claiming the platform does something unmeasured.CLAUDE.md— the paragraph did not say which clone gets a stale tree. Scoped tocore.autocrlf=true, with the reason a Linux or macOS reader should stop reading there.Not in here
Item 6 from #8 — branch protection requiring the
checkstatus — is a repo setting, not a diff, and it decides whether merges get blocked. That's a call for a human, not a side effect of this PR.Items 7 and 8 are known-no-action: the gate tests the branch rather than the merge result, and the cache-warming claim in #6's description did not hold in practice.
Two security findings review raised against this file want their own issue, not a scope grab here:
pull_requeston a self-hosted runner in a public repo is arbitrary code execution from any fork PR, and the cmdline-tools zip is fetched with no checksum. Both landed in #6 and are filed as #12; this PR only edits comments beside them.Follow-ups the review named, deliberately left out: the 13 dependency-freshness warnings still print on every CI run —
lint { disable += "GradleDependency" }is the lever for a quiet gate.allprojects { apply(...) }is cross-project configuration — fine underorg.gradle.configuration-cache(verified, still stores an entry), but a convention plugin is the move if isolated projects ever gets turned on.Answered
concurrencylanded in Forgejo v14.0; this instance is 16.0.3.github.refunderpull_requestisrefs/pull/<n>/head(run #663's own checkout step), so the group is per PR. What stays unproven is that cancellation fires for anypull_requesttype with the explicit block in place — which matters less than first thought, because it already fires forpushwithout it. The comment now says so for every type rather than only forsynchronize.Why
a8d1df2was reworded rather than corrected in a new commit: the wrong claim was its subject line ("entries that never matched anything"), and this branch merges with a merge commit, so it would have gone intomain's history as-is. Tree is unchanged —git diffbetween the pre- and post-rebase heads is empty.No
versionNamebump: nothing user-visible.🤖 Generated with Claude Code
Closing the loop on the open question in the description: run #663 is green on
a8d1df2,event: pull_request, 2m1s.So on Forgejo 16.0.3 the workflow with
concurrencyandtypes:parses and dispatches — the change is not inert and did not break the gate. What run #663 cannot prove is thatcancel-in-progressactually cancels, since one push produces one run and there was nothing to cancel. That needs two pushes in quick succession to demonstrate; I have not manufactured one.Incidental data point for #8 item 8: #663 took 2m1s against #655's 4m31s, so the caches did hit this time. One warm run after one cold run is not a pattern, and it still doesn't show the branch→main direction the original claim rested on — just worth recording next to the "assume ~4m30s cold" note rather than leaving the impression the cache never works.
Re-reviewed
a8d1df2in a detached worktree with a real Android SDK and ran every claim instead of reading it. The mechanical work is good and the verification section is unusually honest — the lint arithmetic reproduces to the character. One thing is wrong, and it is the single line in this diff a human will actually type.Blocking
CLAUDE.md:git add --renormalize .does not fix the tree it is prescribed for. Full repro inline. Short version:--renormalizerewrites the index and never the working tree, and every blob inHEADis already LF (git grep -I -l $'\r' HEAD -- .returns nothing), so in the stale tree it is a no-op with a side effect — it clears theMfromgit statusand leaves./gradlewdying onbad interpreter: /bin/sh^M. The paragraph exists so the next person does not read the rule as broken; as written it hands them a clean status and a build that still will not start.git rm --cached -r . && git reset --hardis what works (verified) — and it discards uncommitted changes, so the sentence has to say so.Two comments that state the platform does something it does not
Both inline, neither harmful to behaviour, both wrong on the merits:
types:is exactly the Forgejo/GitHub default. It saves zero runs; the label/title-edit scenario cannot happen.cancel-in-progressto true forpushandpull_request(synchronize) with no group declared. "Three pushes mean three full cold runs at once" has never been possible here, and this repo's own history proves it: #295 and #323 arecancelled, each superseded ~2 minutes in by the next push. #8 item 1 rests on the same invented cost.Both blocks are still worth keeping — pin the events, declare the group explicitly. Just say what they actually buy.
The open question in the description is answered
github.refunderpull_requeston this instance isrefs/pull/<n>/head. Run #663's own checkout step:So
pr-${{ github.ref }}is genuinely per-PR and thegithub.event.pull_request.numberswap is not needed.concurrencyis supported: it landed in Forgejo v14.0, this is 16.0.3, and the Actions reference listsgithubamong the contexts allowed inconcurrency.group. What remains unproven is only that cancellation fires — which matters less than you thought, because it already fired without the block.Verified and true
Re-ran all of it; the description is accurate here.
spotlessCheckpasses on the head. Four format/check pairs run wheremainruns one::app:spotlessKotlin{,Check},:spotlessKotlin{,Check},:app:spotlessKotlinGradle{,Check},:spotlessKotlinGradle{,Check}. (mainruns:app:spotlessKotlin+:app:spotlessKotlinCheckonly — one pair, not the two the description claims. Cosmetic.):spotlessKotlinGradleCheckwith exactly the diff you quoted. The.ktshalf is enforced.is-properties (getCrunchPngs,getUseProguard,getWearAppUnbundled) before and after. Note for anyone re-checking — you have to pass--no-configuration-cache, otherwise a stored config-cache entry suppresses them and the head appears to have zero.Lint found 13 warnings (and 2 errors, 2 warnings and 1 hint filtered by baseline lint-baseline.xml). Swapmain's baseline back in and it is the same 13 and the same 5, plus12 errors/warnings were listed in the baseline file … but not found in the project. Removing them is exactly a no-op on what lint reports. The 13 are 10GradleDependency+ 2AndroidGradlePluginVersion+ 1ModifierParameter— the 12 dead entries plus the untriaged one, so the argument againstupdateLintBaselineDebugholds too.src/main/...), so unlike the twelve they actually match. Keeping them byte-identical is meaningful, not decorative.abortOnError = truewith nowarningsAsErrors, so the 13 unfiltered warnings cannot turn the gate red..gitattributes: nothing matching png/webp/jpg/jpeg/jks/keystore is tracked, so "not tracked yet" is correct and nothing gets renormalized by adding them.testReleaseUnitTestandcompileDebugKotlinpass.org.gradle.configuration-cache=truestill stores an entry with theallprojectsblock in place.Follow-ups, not for this PR
lint { disable += "GradleDependency" }is the lever; the dead entries were never what printed them.allprojects { apply(...) }is cross-project configuration. Fine underorg.gradle.configuration-cache(verified), but it is exactly what isolated projects forbids — a convention plugin is the move if that ever gets turned on.kotlinGradletarget currently guards two import lines in one file;settings.gradle.ktsandapp/build.gradle.ktshave no imports at all. The LF check and the future module are the real value — worth not overselling..gitattributesgains*.jpeg, which the description's "png/webp/jpg/jks/keystore" list omits.@ -7,1 +7,4 @@# The three events that change the diff, pinned rather than left to the# platform default: a run costs ~4m30s cold, so a default that also fires on# label or title edits would buy one for nothing.The comment asserts something the platform does not do.
opened, synchronize, reopenedis the default — on GitHub by documentation, and on Forgejo by implementation:matchPullRequestEventwith notypes:matches exactlyHookIssueOpened || HookIssueReOpened || HookIssueSynchronized, andHookIssueSynchronizedis mapped to the stringsynchronize. A label or title edit has never started a run in this repo, so this line saves nothing.No objection to pinning it — an explicit set is a reasonable hedge against a future default change, and
synchronize(notsynchronized) is the spelling Forgejo wants, so pushes keep triggering the gate. But the justification in the comment is false, and comments in this repo get read as fact.@ -10,0 +14,4 @@# One runner, self-hosted. Without this, three pushes to a PR mean three full# cold runs at once and the newest — the only one anyone will read — queues# behind two that are already obsolete.Same problem, larger. "three pushes to a PR mean three full cold runs at once" is not something that can happen on this instance. Forgejo already defaults
cancel-in-progressto true forpushandpull_request(synchronize) when noconcurrency.groupis declared — "any previous invocation of the same workflow with the same branch will be canceled".The repo's own run history shows it working, twice: #295
cancelledafter 1m54s with #296 starting 2m06s later; #323cancelledafter 1m33s with #324 starting 1m46s later. Nobody cancelled those by hand.Keep the block — it makes the group explicit rather than implicit, and an explicit
cancel-in-progress: truealso coversopened/reopened, which the default does not. That is the real (small) benefit. As written the comment invents a cost the change never removed, and #8 item 1 rests on the same invented cost.Support itself is fine: concurrency landed in Forgejo v14.0 (Jan 2026), this instance is 16.0.3, and the Actions reference lists
githubamong the contexts allowed inconcurrency.group.@ -39,0 +39,4 @@A clone made before `.gitattributes` existed can still hold CRLF in the filesno later commit touched — git does not re-filter unchanged files on checkout —which fails `spotlessCheck` and stops `./gradlew` from starting at all. Run`git add --renormalize .` once in that tree; it is not a broken rule.git add --renormalize .cannot fix this tree. It applies the clean filter and rewrites the index; it never touches the working tree. And every blob inHEADhere is already LF —— so the clean filter maps the CRLF working copy back onto the identical LF blob, the index does not change, there is nothing to stage, commit or check out, and the CRLF stays on disk.
spotlessCheckstill fails and./gradlewstill will not start.Reproduced against the exact shape #8 item 2 describes (tree created first,
* text=auto eol=lfcommitted after, working copies then set to CRLF):So it is worse than inert: it clears the one signal that something is wrong (
Mingit status) and leaves the build broken. Whoever follows this sentence ends up with a clean status and a./gradlewthat still does not start — which is precisely the "reads as a broken rule" outcome the paragraph exists to prevent.What actually works, same repro:
git reset --harddiscards uncommitted changes to tracked files — the sentence has to say "stash first" or it will cost someone their work in progress. I also triedgit add --renormalize . && git checkout -- .andgit checkout-index -f -a; both leave the CRLF in place. Deleting the tree and re-cloning is the other honest answer.Worth adding: #8's alternative,
./gradlew spotlessApply, cannot work either —gradlewis itself one of the CRLF files.Reworked on
fc4c762— two commits on top of the reviewed head, no code change, both are the words.Blocking —
CLAUDE.md(c1370a8). Reproduced your repro before touching it: scratch repo on git 2.55, tree committed first,* text=auto eol=lfafter, working copies set to CRLF.git ls-files --eolstaysi/lf w/crlfaftergit add --renormalize ., aftergit checkout-index -f -aand aftergit checkout -- .; goes toi/lf w/lfaftergit rm --cached -r . && git reset --hard, and./gradlewruns. The paragraph now names--renormalizeandcheckout -- .as non-fixes and says why (everyHEADblob already LF;--renormalizealso clears theM, which is worse than inert), saysspotlessApplycannot help becausegradlewis CRLF, and giveswith a fresh clone as the alternative. Checked the stash round trip too: a CRLF-terminated edit in progress survives and lands as LF. Plain
git stashis enough —reset --hardonly touches tracked files, so untracked work is not at risk.types:comment (fc4c762). Now says it is exactly the platform default, pinned so a future default change cannot widen the trigger, and that it saves no runs today. The invented label/title-edit cost is gone.concurrencycomment (fc4c762). Now says Forgejo already cancels supersededpush/synchronizeruns with no group declared (cites #295/#296 and #323/#324), that the block makes the group explicit and extendscancel-in-progresstoopened/reopened, and recordsgithub.ref=refs/pull/<n>/headso the per-PR claim is not left to the reader.Description. Rewritten to own all of it: the concurrency bullet no longer claims a cost that never existed and says #8 item 1 rested on the same mistake; the CRLF bullet tells the renormalize story straight; "four pairs where there were two" is now "where
mainruns one";*.jpegis in the list; thekotlinGradletarget's actual coverage today (two import lines in one file) is stated rather than oversold;--no-configuration-cacheis noted next to the deprecation-warning check; your follow-ups (GradleDependencydisable, convention plugin under isolated projects) are recorded as deliberately out of scope.Verification on the rework.
spotlessCheckpasses onfc4c762; the workflow still parses to the sameon:andconcurrency:values. Did not re-run lint/tests — the diff sincea8d1df2is one Markdown paragraph and two YAML comment blocks.Follow-ups. Left out as you suggested. Also correcting #8 in a comment there: item 1's premise and item 5's entry count (17, not 18).
Requesting re-review.
Re-reviewed
fc4c762by running it: real./gradlewin throwaway worktrees against the local Android SDK (JDK 21 here, CI is 17), plus the CI log for run #666.The mechanical work is right and the note-by-note coverage is faithful — I checked your transcription against my own #6 review and all eight notes are there, grouped correctly, nothing invented or dropped. Four of the five actionable ones are done. The fifth is still wrong, and two of the rationales this PR exists to correct are themselves inaccurate.
Blocking
CLAUDE.md:51,53— the stash wrapper is wrong in the default case and destructive in a common one. I was wrong in my last review to accept the shape of this paragraph without running the whole recipe; the middle line is correct, the wrapper is not.Reproduced:
git clone -c core.autocrlf=trueat the pre-.gitattributescommit, then checkout this head. 56 filesw/crlf,gradlewshebang#!/bin/sh\r\n— andgit status --porcelainreturns zero paths, because thetext=auto eol=lfclean filter normalizes CRLF→LF on comparison. So:git stashprintsNo local changes to saveand creates no entry.git rm --cached -r . && git reset --hardfixes the tree (97w/lf, LFgradlew).git stash popthen fails withNo stash entries found.(exit 1) — or, if the user has any unrelated stash entry, silently applies and drops it. I planted one and watched it land in the index and vanish fromgit stash list.The paragraph exists so the next person does not read the rule as broken. As written it ends in a spurious error, or in someone else's WIP appearing in their tree.
Make stashing a precondition rather than a step: "commit or stash anything you care about first — the next line discards uncommitted changes", then the single
git rm --cached -r . && git reset --hard.CLAUDE.md:44-46— the justification is false. "--renormalizealso clears theMfromgit statusthat was the only sign something was wrong": there is noM.git status --porcelainis 0 paths before and aftergit add --renormalize ., with all 56 files still CRLF. That was my error originally and this PR inherited it. The true version is a stronger argument for the paragraph: nothing ingit statusever signals the problem, which is exactly why it reads as a broken rule.Non-blocking
.forgejo/workflows/pr-checks.yml:15-19still claims more than the evidence carries. "Forgejo already cancels a superseded run for push and synchronize with no group declared (this repo: #295 cancelled by #296, #323 by #324)" — both cited pairs areevent: pushruns on main. Nopull_requestrun in this repo has ever been superseded; the four that exist are 20+ minutes apart. The body concedes cancellation is unproven, so the comment should too: scope the parenthetical topushand mark thesynchronizehalf as inferred. My own note c and #8 item 1 were the original source of this over-claim, so this is the third pass on the same sentence — worth getting exact.The lint baseline entries were not dead, and removing them is not "exactly a no-op". Lint resolves
$HOMEin baseline paths, and the twelve deleted entries point at$HOME/Projects/git.imhof.cloud/OpsDeck/mobile/…— my own primary checkout, not "another machine's". I put main's baseline into a tree under$HOMEwith those paths repointed at it: lint goes from13 warnings (2 errors, 2 warnings and 1 hint filtered)to5 warnings (2 errors, 10 warnings and 1 hint filtered). Eight of the twelve match when the path resolves; the other four are dead because their version strings moved on, not because of the path.So the change is a no-op in CI and in a detached worktree — which is all either of us measured — but it makes local lint noisier, the opposite of what the note wanted. No gate risk: all eight are warnings, and
abortOnError = truewith nowarningsAsErrors. Just fix the claim to "no-op in CI".Pre-existing, not this PR, but this PR is editing the trigger: public repo +
pull_request+ self-hostedcth-ubuntu-latestrunning./gradlewfrom the PR head is arbitrary code execution on the runner from any fork PR. The new comment reasons about a future default change "widening the trigger" while the live exposure goes unmentioned. Same file, the SDK stepwgets the cmdline-tools zip with no checksum. Both landed in #6; they deserve an issue, not a scope grab here.Verified and true
spotlessCheckgreen on the head. Four pairs run wheremainruns one — confirmed by--dry-runon both::spotlessKotlin{,Check},:spotlessKotlinGradle{,Check},:app:spotlessKotlin{,Check},:app:spotlessKotlinGradle{,Check}vsmain's:app:spotlessKotlin{,Check}. "main runs one" is right..ktshalf is enforced, and more broadly than you tested. Prepending out-of-order imports tosettings.gradle.ktsfails:spotlessKotlinGradleCheck; doing it toapp/build.gradle.ktsfails:app:spotlessKotlinGradleCheck. Both halves die.lintDebuggreen,Lint found 13 warnings (and 2 errors, 2 warnings and 1 hint filtered), and the "12 not found" line is gone. The arithmetic reproduces exactly.src/main/...paths and do match. Keeping them byte-identical is meaningful. The argument againstupdateLintBaselineDebugholds.types:does not breaksynchronize. Run #666 is green on this exact head (fc4c762,event: pull_request, refspecrefs/remotes/pull/9/head) — so the pin is live and harmless, andgithub.refreally is per-PR. Worth noting the body still cites #663/a8d1df2as the green run; #666 is the current one.gradle/libs.versions.tomluntouched, spotless still 8.9.0,ktlint("1.8.0")and the layout map carried over byte-identical into both thekotlinandkotlinGradleblocks. No dynamic versions, no new repositories, no softened checks — the baseline removal un-silences findings rather than hiding them..gitattributes: nothing matching png/webp/jpg/jpeg/jks/keystore is tracked, so nothing renormalizes.cc702a6in locally — clean, ort — andspotlessCheck,testReleaseUnitTestandlintDebugall pass with the same 13 warnings.Fix the
CLAUDE.mdrecipe and the two rationales and this is good to go.fc4c7627ae2e962465a4Both blocking items fixed, the non-blocking claim fixed, and the security pair split out to #12. You were right on all three, and on the lint one you were more right than the note said — see below.
Force-pushed:
fc4c762→2e96246, anda8d1df2→dde4f70is a reword, not a rewrite. Tree is byte-identical across the rebase (git diffbetween the old and new heads is empty). Run #679 is green on2e96246;spotlessCheck,lintDebugandtestReleaseUnitTestalso pass locally.Blocking
CLAUDE.md— the stash wrapper is gone. Reproduced your repro before touching the paragraph. git 2.55,git clone -c core.autocrlf=true --no-checkout, checkout20985db^, then checkout this head:w/crlf,gradlewshebang#!/bin/sh^M,git status --porcelain→ 0 paths.git stash→No local changes to save, no entry.git rm --cached -r . && git reset --hard→ 97w/lf, 1w/crlf(the.bat, as intended),gradlewshebang#!/bin/sh.git stash popwith an empty stack →No stash entries found., exit 1. With an unrelated entry planted first → applied it into the working tree and dropped it fromgit stash list. Watched it happen.5da20e0makes stashing a precondition in a comment above the single command, exactly as you suggested, and adds one sentence naming the wrapper as a thing not to re-add — this is the third pass on this paragraph and the next person should not have to rediscover why.The
Mjustification is gone too, and you are right that the truth is the stronger argument.git add --renormalize .: 91 files CRLF before and after, 0 status paths before and after. Nothing ingit statusever signals the problem, which is precisely why the rule reads as broken. That is what the paragraph now says.Non-blocking
The cancellation claim is scoped to
push(2e96246). Confirmed your reading against the run list: this repo has exactly four cancelled runs ever — #138, #158, #295, #323 — and all four areevent: push. Nopull_requestrun has ever been superseded. The comment now cites the two pairs and labels themevent: pushon main, and marks thesynchronizehalf as assumed and unmeasured.The lint baseline claim was wrong in the commit subject, so the commit got reworded rather than patched. You are right that lint expands
$HOMEat read time and that "another machine's checkout" was false. Reproduced by droppingmain's 17-entry baseline into a worktree at exactly$HOME/Projects/git.imhof.cloud/OpsDeck/mobile:main(17)main(17)Your 8-of-12 number lands exactly.
dde4f70now says: no-op in CI, un-silences 8 dependency-freshness warnings in that one checkout, 4 genuinely dead on version drift, no gate risk. It also carries yourlint { disable += "GradleDependency" }note as the actual lever for a quiet gate.I reworded rather than adding a follow-up commit because the false claim was the subject line — "entries that never matched anything" — and this branch merges with a merge commit, so it would have gone into
main's history as written. Sorry for the churn to any worktree you had offfc4c762.Split out
#12 covers both security findings:
pull_requeston a self-hosted runner in a public repo is untrusted code execution from any fork PR, and the cmdline-tools zip is fetched with no checksum (same two lines exist inbuild.yml, which does touch the keystore). Neither is introduced here — this PR only edits comments beside them — and the fork-PR one needs a decision, not a patch, so it did not belong in this diff. Your point that the trigger comment reasons about a hypothetical future widening while ignoring the live exposure is quoted in the issue.Not taken
The three follow-ups you marked as not-for-this-PR are still not in it: the
GradleDependencydisable, the convention-plugin move for isolated projects, and thekotlinGradletarget's honest scope (which the description already states rather than oversells).The description is rewritten against the current head, including the run number — you were right that it still cited #663/
a8d1df2.Re-reviewed
2e96246by running it rather than reading it: throwaway worktrees, real./gradlewagainst the local Android SDK (JDK 21 here, CI is 17), and a freshcore.autocrlf=trueclone for the CRLF recipe. Both blocking items are fixed and both fixes reproduce. Approving.The two blockers, run-confirmed
CLAUDE.md:52-55— the stash wrapper is gone and the single command is right. Reproduced the setup first:git clone -c core.autocrlf=true --no-checkout, checkout20985db^, then this head — 91 filesw/crlf,gradlewshebang#!/bin/sh\r\n,git status --porcelainzero paths, and./gradlewexiting 126 withbad interpreter: /bin/sh^M. Then, with an unrelated entry planted on the stash stack, the prescribed line verbatim:97
w/lf, 1w/crlf(gradlew.bat, as.gitattributesintends), shebang#!/bin/sh,./gradlew --version→ Gradle 8.14.3, andstash@{0}still on the stack, unapplied. The precondition holds for real work in progress too: a CRLF-terminated content edit is visible togit status, stashes, and comes back as LF afterpop.The
:57-60warning is true as written, and I checked both halves instead of taking it:popon an empty stack exits 1 withNo stash entries found.; with one unrelated entry it applied that entry into the working tree and dropped it fromgit stash list.CLAUDE.md:42-49— theMclaim is gone, and the replacement is true rather than reworded.git add --renormalize .: 91 files CRLF before and after, 0 status paths before and after.git checkout -- .: identical.git grep -I -l $'\r'over the head tree returns nothing, so "every blob inHEADis already LF" is right. Thefails spotlessCheckhalf of the first sentence also holds — running the head's wrapper with-pat the stale tree gives:spotlessKotlinGradleCheck FAILED.The non-blocking claims
Cancellation is now scoped to what the run list carries (
pr-checks.yml:15-21). Checked against the runs, not the prose: exactly four cancelled runs exist in this repo — #138, #158, #295, #323 — allevent: push; #296 and #324 start about two minutes after the runs they superseded, andcd39e66/31ff51eare both ancestors ofmain. Sevenpull_requestruns exist (#655, #663, #665, #666, #667, #678, #679) and none is cancelled. Accurate.The lint baseline claim is correct, and the mechanism is now proven rather than inferred.
dde4f70says no-op in CI, un-silences 8 in that one checkout — both reproduce. This time I tested the$HOMEexpansion itself instead of repointing paths: a worktree at$HOME/.cache/opsdeck-r2-review/mobile, baseline entries written as the literal$HOME/.cache/opsdeck-r2-review/mobile/…, and lint reports5 warnings (and 2 errors, 10 warnings and 1 hint filtered)with4 … not found. So lint does expand the token at read time: 8 of 12 match, 4 are dead on version drift.Methodology note for anyone re-checking, because it nearly talked me out of my own finding: overriding
HOME=in the environment does not move this. OpenJDK takesuser.homefrom the passwd entry, not from$HOME, so an env override reports the CI numbers (13 warnings, 12 not found) and looks like a disproof. Put the tree at a real path under the real home instead.updateLintBaselineDebugdoes write 18 entries. With the baseline emptied, lint's full finding set is2 errors, 15 warnings, 1 hint— exactly the 13 reported plus the 2 filtered.The security pair is correctly out of scope. #12 states both findings accurately, including that the same unchecked
wgetsits inbuild.yml, which does touch the keystore. Nothing here widens the exposure:on:moves from barepull_requestto the three types that were already the default, so the effective trigger is unchanged.Citations fixed. #679 is
successon2e96246,event: pull_request.a8d1df2anddde4f70are byte-identical trees (git diffbetween them is empty), so the reword claim holds.Re-verified, no regressions
spotlessCheck,lintDebugandtestReleaseUnitTestall green on2e96246. Lint:13 warnings (and 2 errors, 2 warnings and 1 hint filtered), and no "listed in the baseline … but not found" line.main, compared by--dry-runon both..ktshalf is enforced in both projects: out-of-order imports prepended tosettings.gradle.ktsfail:spotlessKotlinGradleCheck, and toapp/build.gradle.ktsfail:app:spotlessKotlinGradleCheck.gradle/untouched, spotless 8.9.0, bothktlint("1.8.0")pins and the layout map identical, no new repositories,FAIL_ON_PROJECT_REPOSintact,abortOnError = truewith nowarningsAsErrors, no secrets.-textpatterns.mainunder--warning-mode all --no-configuration-cache: the same three AGPis-properties.main; mergingcc702a6locally is clean (ort) and green, with the same 13 warnings.main's only overlapping edit isversionName 0.31.0 → 0.31.1, and no spotless residue is left inapp/build.gradle.kts.Two nits, neither worth another pass
pr-checks.yml:19-20: "extends cancel-in-progress to opened and reopened, which the implicit default does not cover" is the last flat assertion about implicit behaviour in that comment, and it is as unmeasured here as thesynchronizehalf two lines above that now says "assumed" — noopened/reopenedrun in this repo has ever been superseded either. Hedge it or drop the clause if the file is touched again; the sentence has had four passes and does not need a fifth for this.CLAUDE.md:39: the paragraph does not say the stale tree only comes from a clone made withcore.autocrlf=true. On a Linux or macOS default a pre-.gitattributesclone already holds LF, so a reader there can follow the paragraph and run the one command in this repo that discards uncommitted work while chasing a problem they cannot have. One clause fixes it — take it whenever, not now.