bug(windows): three path gaps - the #181 version_files guard misses ..\ paths, recover-pending-cut joins an absolute changelog, an atomic write fails over a read-only file #1526
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
frankenbit/release-toolkit#1526
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Three Windows-only path gaps, listed as out of scope in #1502 because they had only been found by reading the code. All three are now measured. Each comes from treating
/as the only separator or the only marker of an absolute path, or from relying on POSIX rename semantics.Measured 2026-10-02 at
96d2646, on an LF clone. Windows: Go 1.27.1 windows/amd64, native, from Git Bash. Linux: go1.25.14 linux/amd64 ingolang:1.25.1. The
version_filesrepo-relative guard (#181) misses\pathsFixture: repo
Rwhoserelease-toolkit.ymlhasversion_files: ['..\outside/VERSION'], and a siblingR\..\outside\VERSIONcontaining9.9.9.Windows, native
rt manifest-checkinR(exit 0):It reads the version from a file outside the repository.
Windows, native
rt prep --dry-runinR, withTMPpointed at a fixture directory. The dry run copies the repo to%TMP%\rt-prep-dryrun-*, so the entry resolves to%TMP%\outside\VERSION. That file held7.7.7and afterwards held0.1.0. The disclosure line does not name it:A real
rt prepjoins the same entry to the checkout root (filepath.Join(repoRoot, vf),cmd/rt/prep.go:345). It would therefore writeR\..\outside\VERSION. That is read from the code, not run.Linux, same fixture:
config valid, thenFAIL: version_file missing: ..\outside/VERSION. There\is an ordinary filename byte, and the entry names a directory inside the repository.Cause.
Load(internal/config/config.go:194) tests for a leading/, andhasTraversal(:349-353) tests for..between/separators. On Windows\is also a separator, and a drive letter or a leading\makes a path absolute or rooted. The basename guard does not help, becausepath.Basestill returnsVERSIONwhenever the last separator is/.2.
resolveUnderRoottreats only a leading/as absolutecmd/rt/recover_pending_cut.go:579, native on Windows:On Linux the absolute path comes back unchanged. The doc comment promises "the same rule decide.resolve applies", and
decide.resolve(internal/decide/decide.go:1362) usesfilepath.IsAbs. On Windows, with an absolutechangelog:,decidetherefore reads the right file andrecover-pending-cutdoes not. By the code at:245-250, the verb then stops withcannot read <changelog>(cannot-grade) where Linux proceeds.3. An atomic write fails over a read-only destination
Three writers rename a same-directory temp file over their destination:
writeFileAtomic,internal/release/cutter.go:637: the cut's CHANGELOG seal and its rollback;writeRecoveryFileAtomic,cmd/rt/recover_pending_cut.go:532;Write,internal/manifest/store.go:253. #1502 did not list this one, but it has the same shape.With the destination at
chmod 0444(on Windows that setsFILE_ATTRIBUTE_READONLY):writeFileAtomicrename ...\.rt-tmp-2366756064 ...\CHANGELOG.md: Access is denied.-r--r--r--writeRecoveryFileAtomicrename ...\.rt-recover-2233725798 ...\CHANGELOG.md: Access is denied.-r--r--r--Writemanifest: rename ...json.tmp -> ...json: ... Access is denied.-rw-r--r--On Windows the destination keeps its old content, and no temp file is left behind. Go's Windows
os.Removeclears the read-only attribute and retries (src/os/file_windows.go,func Remove), so the deferred cleanup still deletes a read-only temp file.os.Renamehas no such retry.git does not record the read-only attribute, so a checkout never produces this state. It takes a deliberate
attrib +R, or a tool that sets the attribute.Why it matters
rtread or write outside the repository. On Windows, one entry does both. The--dry-runpreview writes outside its scratch copy and does not report the write.recover-pending-cutis the operator's recovery verb. On Windows it cannot grade a repository whosechangelog:is absolute, althoughdecidereads the same path correctly.Out of scope
\inversion_fileson every OS. That would change what Linux accepts, and whether a checked-in config must be portable is a separate question.Acceptance criteria
config.Loadrejects aversion_filesentry that leaves the repository or is not local when\is read as a separator: a..segment, a leading\, a drive letter or a UNC prefix. On Linux, the same entries are accepted or rejected as before, with the same messages. A test fails on Windows before the change and is mutation-verified. — DONE @ #1527 (2fe9db5and81086ff, reviewed as8086700).TestLoad_versionFilesBackslashPathsfailed natively on Windows before each commit, and mutants M1a, M1b and M1c are each killed there. On Linux the same entries are graded as before: 51 of 51rtruns at8086700are byte-identical todd871e8, messages included.resolveUnderRootreturns an OS-absolute path unchanged, asdecide.resolvedoes. Linux output is unchanged. A test fails on Windows first and is mutation-verified. — DONE @ #1527 (b46db1d, reviewed as8086700).TestResolveUnderRootLeavesAnAbsolutePathAlonefailed natively on Windows first, and mutant M2a (back to a leading-/test) is killed there. On Linuxfilepath.IsAbsisHasPrefix(path, "/")(Go 1.27.1 source).5ca2973and8086700, reviewed as8086700). Chosen: clear the attribute and retry, on Windows only, with the attribute put back if the retry fails. The decision tree is in #1527's body. The three writer tests andTestRename_replacesAReadOnlyDestinationfailed natively on Windows withAccess is denied.first. Mutants M3a to M3f are each killed there.go vet ./... && go test -count=1 ./...passes natively on Windows (Git Bash) and ingolang:1.25. On Linux, the output of the touched verbs is byte-identical before and after. — DONE @ #1527 (8086700, reviewed as8086700).go vetis clean, andgo testpasses 30 of 30 packages natively from Git Bash and ingolang:1.25. Linux:--help,manifest-check,prep --dry-runandrecover-pending-cut --dry-runare byte-identical todd871e8over 51 runs. Those runs did not reachrecover-pending-cut's changelog write, the cut's seal or a manifest write. Those lines are identical off Windows by construction, and unit tests passing ingolang:1.25pin them.changelog.dfragment for the change. — DONE @ #1527 (2fe9db5, reviewed as8086700).changelog.d/1526-windows-path-gaps.fixed.md;rt fragment-checkin content mode passes.Anchor
Split from #1502's out-of-scope list. The landed Windows build (
96d2646) made a native measurement possible.changelog:is read as given by decide and manifest-check, but prep and release join it under the repo root #1528All five acceptance criteria are ticked against #1527, and this issue is now shut by hand (AGENTS.md section 15).
8086700b1a83ec766f58a29579af79158ccb3729, a fast-forward merge pinned to the approved head.merge_commit_shaequals the reviewed head, andgit range-diff dd871e8..8086700 dd871e8..origin/mainshows all 5 commits as=.8086700. The verify-at-source verdict is review 7208, at the same head.8086700: 30 of 30 contexts green, andac-closure-checkandbase-divergence-checkpassed. I also checked the PR comments by hand: none has a closing keyword before an issue number.8086700were 18 success and 12 pending, none failed, when this was written.claude/1526-windows-path-gapswas deleted by the merge; the forge returns 404 for it.Follow-up, filed and not scheduled: #1528, where
prepandreleasejoin an absolutechangelog:under the repo root whiledecideandmanifest-checkread it as given.