Skip to content

v1.12.4 - a failed folder create frees the global lock, and a tracked-file keep refusal says why

Choose a tag to compare

@github-actions github-actions released this 16 Oct 06:47
· 60 commits to main since this release
v1.12.4
343d0e4

A failed folder create frees the global lock, and a tracked-file keep refusal says why

Small fixes from CodeRabbit's review of the v1.12.3 range (thread t114) and from a flaky test teardown. Nothing a user could do before is taken away, and nothing new is exposed. The installed plugin moved: scripts/lib/apply.mjs, scripts/lib/keeps.mjs, scripts/lib/repo-fs.mjs (a comment only), skills/coalwash/references/method.md and their plugin/ twins.

Fixed

  • The global lock is released when the transaction folder cannot be created. applyPlan took the global lock, then ran mkdirSync on .claude/coalwash; a throw there (for example EACCES) left the lock held until its 30-minute stale window, and every other run deferred meanwhile. The create is now inside a try: on a throw the lock is released and the run returns { ok: false, error: 'apply: cannot create the transaction folder (<code>)' }. — test: scripts/lib/apply.test.mjs, global-scope lock: a transaction folder that cannot be created refuses the run AND releases the global lock
  • A bin-stash failure no longer blames the lock for every cause. recordBinItem returns null for several causes (its per-bin lock was busy, the bin's catch-all .gitignore, the bin folder, or the item or index write failed), but the flag applyPlan raised said only that the lock could not be acquired. It now reads "recordBinItem could not record it (its per-bin lock was busy, or the catch-all .gitignore of the bin, the bin folder, or the item or index write failed)". The recoverDangling partial-recovery message likewise says "refused as out-of-root or as a create whose copy could not be binned": its count (refusedOutOfRoot) also counts a create that could not be binned, and the field name is unchanged because a test pins that count. — test: scripts/lib/apply.test.mjs, fire 63: a bin-stash failure that is NOT the lock is flagged without blaming the lock
  • Contributor-facing: CodeQL alerts #53 to #56 (js/file-system-race, self-ignore.test.mjs). The test read a file's inode by path and then wrote and read the same path in a loop, which the analyzer reads as a check followed by a use. The inode is now read through an open file descriptor. A single-process fixture had no second actor; the alert was a shape, not a race. The two junction tests also probe whether the volume can make a junction and skip visibly when it cannot, and the old dual-outcome directory-link test is split into two tests that each decide their branch by probing whether a rename over a directory link is refused. — test: scripts/lib/self-ignore.test.mjs
  • Contributor-facing: the EBUSY teardown flake on the Windows CI leg (PR #22, job 116559157309). apply.test.mjs's ENOBUFS fail-closed test (R14 D3 bounce 2) removed its temp project while a git process still held it. Git for Windows runs a launcher, cmd\git.exe, which starts the real git.exe; when spawnSync kills the launcher on ENOBUFS, the real process lives on for 2.9 to 7.6 s (n = 6, measured on one box) with its working folder inside the project, and Windows will not remove a held folder. The cure is in the teardown: removeTree in scripts/lib/test-plan.mjs retries a removal that fails with EBUSY, EPERM or ENOTEMPTY with fresh calls for up to 30 s and throws after that, so a real leak stays loud. The 32 teardown removals in seven test files go through it. The shipped engine is unchanged: its one git spawn (git --no-optional-locks ... ls-files -z) was measured to write nothing in the project, 3 of 3. One residual remains and is named: on Windows, after an ENOBUFS or timeout kill of the Git for Windows launcher, that read-only git ls-files can hold the user's folder busy for a few seconds (2.9 to 7.6 s, n = 6), and it writes nothing. — test: scripts/lib/test-plan.test.mjs, removeTree removes a temp project while a child process still holds it as its cwd

Changed

  • recordKeep now returns the reason it refused, in an error field. The result keeps its status fields (ok, anchorDropped, anchorStored) unchanged and gains error on a failed write; a refusal reads tracked: ... or self-ignore: ... (the same prefixes applyPlan uses), and the sandbox-folder refusal carries one too. The early guard returns (no target, a newer schema) are unchanged. The field is additive and no shipped code calls recordKeep: its callers are tests. The v1.12.3 entry and the SECURITY.md record name these refusals; until v1.12.4 the caller of recordKeep could not read the reason. — test: scripts/lib/self-ignore.test.mjs, the two L58-1 keeps legs
  • references/method.md §3, item 2 (and its dist twin) says what the catch path returns. It said the early guards and the catch path share the shape { ok: false, anchorDropped: false, anchorStored: false }; the catch path now also carries error.
  • Forward note on the [1.12.3] entry (CodeRabbit t114). That entry and the SECURITY.md record cite apply.mjs:786 for the planted-.gitignore flaw. That is the line at v1.12.2, where PR #18's thread was filed (inside acquireLock's stale-takeover branch by v1.12.3); the function is ensureSelfIgnore, which delegates to ensureCatchAllIgnore in repo-fs.mjs. The released entry is not edited.