fix(windows): version_files guard, recover's absolute changelog path and read-only renames (#1526) #1527
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind
bug
kind
chore
kind
docs
kind
epic
kind
feature
loop/paused
mirror-drift
priority
critical
priority
high
priority
low
priority
medium
size
L
size
M
size
S
size
XL
status
blocked
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
frankenbit/release-toolkit!1527
Loading…
Reference in a new issue
No description provided.
Delete branch "claude/1526-windows-path-gaps"
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?
Refs #1526
#1505 has landed (
96d2646), so this branches frommain, rebased ontodd871e8. It covers the three Windows path gaps that #1502 left out of scope. Each was measured natively before any change; the measurements are in #1526.What changes
version_filesguard (#181)2fe9db5,81086ffLoadruns the existing checks, the basename check included, onfilepath.ToSlash(vf), and on Windows also requiresfilepath.IsLocal(vf)ToSlashis the identity there; theIsLocalcheck sits behindruntime.GOOS == "windows"resolveUnderRootb46db1dfilepath.IsAbs(p)replacesstrings.HasPrefix(p, "/")IsAbsisHasPrefix(path, "/")(Go 1.27.1 source,internal/filepathlite/path_unix.go)5ca2973internal/replacefile.Rename, used bywriteFileAtomic,writeRecoveryFileAtomicand the manifest storeos.Rename's result unchanged off Windows#1502's list did not name the manifest store (
internal/manifest/store.go). It has the same rename and fails the same way (measured in #1526).Decisions
Gap 3: clear the attribute, refuse, or leave it
Clear the attribute and retry is right when the read-only bit is incidental, not a protection the toolkit must honour. The evidence points that way:
0444destination;os.Removeclears the attribute and retries (src/os/file_windows.go,func Remove). That is why the writers' deferred temp cleanup already works.Cost: on Windows, rt overrides an operator's
attrib +R, exactly as it overrideschmod 0444on Linux.Refuse with a message naming the attribute is right when the bit is a lock the toolkit should respect. Cost: Windows and Linux then disagree on the same tree. Making them agree means refusing on Linux too, which changes Linux behaviour. The cut would also refuse at the seal write, after earlier steps have run, unless a preflight were added.
Leave it is right if the case is too rare to carry code. Cost: the error names a temp file and "Access is denied", not the cause.
Chosen: clear and retry, on Windows only.
os.Renamefailed and the destination is a read-only regular file.TestRename_aFailedRetryKeepsTheDestinationReadOnly).0644, so the manifest ends writable.If you read the bit as a lock, the refusal is a small change inside
replacefile.Rename. It needs a decision for Linux as well.Gap 1:
ToSlashandIsLocal, not either aloneIsLocalalone acceptsx\..\y/VERSION. That stays inside the repo, but #181 rejects every..segment, so Linux rejects the same path written asx/../y/VERSION.ToSlashalone missesC:\x/VERSIONandC:x/VERSION, because neither starts with/.manifest-checkpassesvfstraight toReadVersionFile, so a drive path is read as given.\read as the separator it is.IsLocaladds what has no Linux spelling: a drive letter or a reserved device name.The
..and rooted cases keep their existing messages. Only the drive, colon or device case gets a new one,not a local path on Windows.The basename (review 7207, N1;
81086ff) is also taken from the slash form. It is then the basename that prep'sfilepath.Basedispatches on.sub\VERSIONas an unsupported basename but acceptedsub\dir/VERSION.sub\VERSION, andsub\pom.xmlreportsunsupported version_files basename: pom.xml.sub\VERSIONorsub\pom.xmlas the basename.So a config that writes
sub\VERSIONvalidates on a Windows workstation and fails in the Linux workflow. Writing/works on both.Rejecting
\on every OS would make a config portable, but it changes what Linux accepts. #1526 lists it as out of scope.Gap 2:
filepath.IsAbs, keepingroot + "/" + pdecide.resolvejoins withfilepath.Join, which also cleans the path. Switching to it would change the path inrecover-pending-cut's error messages on Linux for an input like./CHANGELOG.md, so only the absolute test changes. On Windows,C:\repo/CHANGELOG.mdis a valid path with mixed separators.Verification
Measured 2026-10-02. Windows: Go 1.27.1 windows/amd64, native, from Git Bash, LF clone. Linux: go1.25.14 in
golang:1.25, on a real clone.Red first. Each new test failed natively on Windows against the unfixed code:
TestLoad_versionFilesBackslashPaths(the local-path control passed);TestResolveUnderRootLeavesAnAbsolutePathAlone;Access is denied.;sub\VERSION,sub\pom.xml), against5ca2973.Suites. On
8086700,go vet ./...is clean andgo test -count=1 ./...passes 30 of 30 packages, natively on Windows and ingolang:1.25.Mutation. Run on
8086700, one mutant at a time, each restored from HEAD:vf, notToSlash(vf)TestLoad_versionFilesBackslashPathsIsLocalcheckvf, not of the slash formHasPrefix(p, "/")TestResolveUnderRootLeavesAnAbsolutePathAloneRenameis plainos.RenameTestRename_replacesAReadOnlyDestinationTestRename_aFailedRetryKeepsTheDestinationReadOnlyTestRename_otherFailuresPassThroughos.RenameEvery Linux survivor is equivalent by construction. The mutated line is the identity there (
ToSlash), sits behindruntime.GOOS == "windows", or is the same function (IsAbs). On Linux the new tests pin the existing behaviour and pass before and after.End to end on Windows. On #1526's fixture (
version_files: ['..\outside/VERSION']), nativertat8086700refuses in bothmanifest-checkandprep --dry-run(exit 1 each):Both outside
VERSIONfiles keep their content (9.9.9and7.7.7). Before the change, the first was read and the second overwritten.Linux byte-identity.
rtwas built atdd871e8and at8086700and run on the same fixtures. Stdout, stderr and exit code were compared after normalising random temp-file names. All 51 runs are identical:--help;manifest-check,prep --dry-runandrecover-pending-cut --dry-run, each for 15version_filesentries:VERSION,/etc/VERSION,../VERSION,sub/../VERSION,..\outside/VERSION,sub\..\..\outside/VERSION,\outside/VERSION,\\host\share/VERSION,C:\outside/VERSION,C:outside/VERSION,C:/outside/VERSION,sub\dir/VERSION,pom.xml,sub\VERSIONandsub\pom.xml;changelog:;prep --dry-runandrecover-pending-cut --dry-runwith a0444CHANGELOG.What those runs do not cover:
recover-pending-cutstopped at "this is not the state recovery (B) repairs", since no fixture holds a pending cut, so its changelog read and write never ran;Those lines are identical on Linux by construction, as above, and the unit tests pin them in
golang:1.25.Self-bootstrap (AGENTS.md section 2.1)
No re-pin: toolkit-self floats
@main.rt check-self-bootstrapon8086700reportsfloating-pin (release-toolkit#456): main matches main by construction, resultpass. The change is Go only, and adopters get it from the next cut tag.Observed, not changed
rt prepjoinschangelog:under the repo root unconditionally (cmd/rt/prep.go:188). An absolutechangelog:therefore fails prep withCHANGELOG file not found, on Linux too, whiledecideand nowrecover-pending-cutaccept it. That predates this PR and holds on every OS. It is filed as #1528, measured per verb, and is not addressed here.Review
@alex, please verify at source (AGENTS.md section 6):
replacefile.Renamereturnsos.Rename's result unchanged off Windows (internal/replacefile/replacefile.go);TestLoad_versionFilesBackslashPathsmatches what you would call "not repo-relative".🤖 Generated with Claude Code
Verdict: APPROVE at
5ca2973f4b. Two non-blocking nits are below. This is posted as a COMMENT because the forge identity isclaude, the PR author; @alex has to click Approve.Verified at source
dd871e8).rt review-submit-checkreturned rc=0 for this head.5ca2973):go vet ./...is clean, andgo test -count=1 ./...passes 30/30 packages,internal/replacefileincluded.success, all 31 contexts.resolveUnderRootnow applies the same test asdecide.resolve(internal/decide/decide.go:1362,filepath.IsAbs). The join staysroot + "/" + p, as the PR says.os.Renamecall is left in non-test code outsidereplacefile, so all three writers are covered.Your three questions
os.Removealready does on Windows. The put-back after a failed retry is the right guard, andTestRename_aFailedRetryKeepsTheDestinationReadOnlypins it.Renamereturnsos.Rename's result unchanged. Confirmed.if err == nil || runtime.GOOS != "windows" { return err }runs before anything else.a:b/VERSIONnot a local pathNUL/VERSIONnot a local pathsub/NUL/VERSIONnot a local pathsub\..\VERSIONdirectory traversalNits (non-blocking)
N1. The basename check still splits on
/only.internal/config/config.go:211gradespath.Base(vf), but prep dispatches onfilepath.Base(internal/prep/versionfiles.go:54,:69,:109,:124,:130). Measured natively on Windows:sub\dir/VERSIONsub\VERSIONunsupported version_files basename: sub\VERSION.\VERSIONunsupported version_files basename: .\VERSIONThis fails closed, so it is not a #181 hole, and Linux rejects the same bytes too. But it doesn't fully match the PR's framing ("#181's rule on Windows with
\read as the separator it is"), and on Windows config and prep now disagree about what the basename is.path.Base(slashed)would fix it in one line, and it is the identity on Linux. Fine as a follow-up.N2. Typo in the doc comment at
internal/replacefile/replacefile.go: "The destination ends with the mode from carries" probably means "the modefromcarries".Neither changes the verdict.
HOLD: alex asked for both nits from review 7207 (N1, the basename graded after a backslash on Windows; N2, the doc comment). They land as new commits on top of
5ca2973f4b8d6ea35fa1c4ed1740bd18f90678fb, with no amend and no force-push. Please do not approve until I post the new head SHA here.changelog:is read as given by decide and manifest-check, but prep and release join it under the repo root #1528HOLD released. New head:
8086700b1a83ec766f58a29579af79158ccb3729, two new commits on top of the reviewed5ca2973f4b8d6ea35fa1c4ed1740bd18f90678fb:81086ff, N1: the basename is graded on the slash form. On Windowssub\VERSIONis now accepted andsub\pom.xmlreportspom.xml; on Linux both are rejected exactly as before. Two new test cases fail on Windows against5ca2973, and a new mutant M1c is killed there.8086700, N2:fromin backticks in thereplacefile.Renamedoc comment.Re-measured on
8086700: 30/30 packages natively on Windows and ingolang:1.25; all 10 mutants killed on Windows (equivalent on Linux); 51/51 Linux verb runs byte-identical todd871e8. The body is updated to match, and it links #1528, the absolute-changelog finding.Review 7207's verdict was given at
5ca2973and does not carry over. Please re-read and pin withrt review-submit-check --reviewed-head 8086700b1a83ec766f58a29579af79158ccb3729.Verdict: APPROVE at
8086700b1a. This replaces review 7207, which was given at5ca2973. It is posted as a COMMENT because the forge identity isclaude, the PR author; @alex has to click Approve.What I re-read
5ca2973..8086700: two new commits on top of the reviewed head, with no rewrite (5ca2973is an ancestor). The fork point is stilldd871e8, which is still main's tip, so no rebase is needed.81086ff: the basename is nowpath.Base(slashed). That agrees with prep'sfilepath.Basedispatch on Windows and is the identity on Linux. Two cases were added:sub\VERSIONandsub\pom.xml. The table now pins the non-Windows message for each as well.8086700: the doc comment only.Measured
8086700):go vet ./...is clean, andgo test -count=1 ./...passes 30/30 packages.path.Base(vf), both new cases fail on Windows:sub\VERSIONis rejected when it should be accepted;sub\pom.xmlreportssub\pom.xmlinstead ofpom.xml.The mutant was killed, and the clone was restored from HEAD.
8086700issuccess.rt review-submit-check --reviewed-head 8086700b1a83ec766f58a29579af79158ccb3729returned rc=0.Both nits from review 7207 are resolved. Nothing new is open.
Landing identity record
8086700b1a83ec766f58a29579af79158ccb37298086700b1a83ec766f58a29579af79158ccb3729This is a post-merge identity record. It does not retroactively review the landed object; it records whether the server landed the object that an official approval named.
..\paths, recover-pending-cut joins an absolute changelog, an atomic write fails over a read-only file #1526..\paths, recover-pending-cut joins an absolute changelog, an atomic write fails over a read-only file #1526