fix(release): the Windows archive only Windows could open — v0.31.1 #293
No reviewers
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index!293
Loading…
Reference in a new issue
No description provided.
Delete branch "fix-windows-archive-separator"
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?
What happened
v0.31.0 shipped. A user ran the documented Windows install command against a clean machine within hours:
The installer was right. All seven members used a backslash separator — and so did v0.30.1's, and v0.30.0's. APPNOTE 4.4.17.1 says the separator is
/. The supported Windows install path had been broken since v0.30.0, the release that added the refusal, and three releases went out over it with every gate green.Why the writer produced them
release.ymlpacked the archive withZipFile::CreateFromDirectory, having moved to it in #231 specifically to avoid backslashes, recording:Every clause is true except the last, and only on one runtime.
CreateFromDirectorynormalises to/on .NET Core and .NET 5+; on .NET Framework it emitsPath.DirectorySeparatorChar. All ten PowerShell steps declareshell: powershell— Windows PowerShell 5.1 — which is .NET Framework. The fix was real and the hazard correctly named; the premise was wrong about the runtime it ran on.Why nothing saw it — the more useful half
Three instruments, each reasonable, all blind in the same direction:
windows-archive-smokeunpacked withExpand-Archive, the one unpacker that tolerates a backslash — named as such in the comment above, one screen earlier in the same file. Every path assertion after the unpack then passed, because the members had already been written out as a directory tree.installer_ps1_e2e.rsgrades install.ps1's refusals thoroughly — the backslash check has its own test, and its first mutation survived and was strengthened until it bit — but against fixture archives the test builds itself. The check and the malformed artefact were never in the same room.No gate was missing an assertion. They were pointed at synthetic inputs, or made after a normalising step had destroyed the evidence.
The repairs
CreateEntryper file with"$name/$($m.Name)"— a literal/, neverJoin-Path. The step then reopens the finished zip withOpenReadand throws on a backslash or an unexpected root. Asserting what we meant to write would only restate the line that writes it.Expand-Archiveruns only after they pass. Order is the whole claim.windows-archive-smokechecks out the tree and runs the repository's owninstall.ps1against the archive this run just built (-BaseUrltakes a directory — the offline-install path), then runs each of the four installed binaries and matches--versionagainst the tag. This is the step whose absence let it ship three times; it closes the class, not the instance.crates/indexer/tests/windows_archive_shape.rs(5 tests) moves all of it tocargo test. All four mutations run red.Four gates caught my own change, and each was right
a_powershell_block_carries_no_byte_windows_powershell_would_misdecode— I used U+2500 box-drawing characters in a PowerShellrun:block. 5.1 reads a BOM-less script as cp1252, so the byte does not survive; in a double-quoted string the file would not parse. The same family as the bug being fixed.every_workflow_job_measures_free_disk_before_it_builds— adding a checkout put the smoke job in scope, and that rule keeps no exemption list on purpose.every_file_that_names_this_release_is_a_declared_site— two of my new comments named the live version, making them version sites. (This gate, added last release, has now caught me four times.)Three further errors were mine, all one shape — reading prose as structure: the gate matched
Expand-Archivein its own explanatory comment; its job-boundary scan stopped at a two-space comment ending in a colon; andcontains("OpenRead")was satisfied by a mutation that left the word in a comment. Every assertion there now requires a line that runs. Separately, three steps share the namePackage release archive, so the first draft graded the linuxtarstep.version_bump.shcalled a complete bump incompleterusqlitesits at 0.31.0, so after all thirteen of our crates correctly moved to 0.31.1,Cargo.lockstill contained the string and the self-check exited 1. Lockfiles are now exempt from that half only — they must still state the new version, so a lockfile cargo failed to rewrite is still caught. Verified by a round-trip bump in a copy of the tree. Second time that one coincidence has had to be designed around; the first is why cargo owns the lockfiles at all.Gates
Green on one unchanged tree (hash recorded before and after):
cargo fmt --all --checkcargo clippy --workspace --all-targets -D warningscargo doc(-D warnings,--document-private-items)--locked)For users on v0.31.0 or earlier
Expand-Archivetolerates the malformed archive, so a manual unpack works and the published.sha256still verifies the bytes. v0.31.1 installs normally.No schema migration, no plugin activation change, no change to any published catalog field.
🤖 Generated with Claude Code
https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu
v0.31.0 shipped, and a user ran the documented Windows install command against a clean machine within hours: install.ps1: REFUSING THE ARCHIVE: `code-index-v0.31.0-windows-x86_64\.code-index.toml.example` contains a BACKSLASH. The installer was right. Every one of the archive's seven members used a backslash separator, and so did v0.30.1's, and v0.30.0's. APPNOTE 4.4.17.1 says the separator is `/`. The supported Windows install path had been broken since v0.30.0 -- the release that ADDED the refusal -- and three releases went out over it with every gate green. WHY THE WRITER PRODUCED THEM. `release.yml` packed the archive with `ZipFile::CreateFromDirectory`, having moved to it in #231 SPECIFICALLY to avoid backslashes, recording that `ZipFile` "writes '/' unconditionally". True on .NET Core and .NET 5+. Every PowerShell step in that file declares `shell: powershell` -- Windows PowerShell 5.1, on .NET FRAMEWORK -- where it emits `Path.DirectorySeparatorChar`. The fix was real, the hazard was named correctly, and the premise was wrong about the runtime it ran on. WHY NOTHING SAW IT, which is the more useful half. Three instruments, each reasonable, all blind the same way: * `windows-archive-smoke` unpacked with `Expand-Archive` -- the ONE unpacker that tolerates a backslash, named as such in the comment above, one screen earlier in the same file. Every path assertion after the unpack then passed, because the members had already been written out as a directory tree. * `installer_ps1_e2e.rs` grades install.ps1's refusals thoroughly, but against FIXTURE archives it builds itself. The check and the malformed artefact were never in the same room. * The writer asserted nothing about what it had written. No gate was missing an assertion. They were pointed at synthetic inputs, or made after a normalising step had destroyed the evidence. THE REPAIRS: * Entry names are built by hand as `"$name/$($m.Name)"` -- a literal `/`, never `Join-Path` -- and the step then REOPENS the finished zip and throws on a backslash or an unexpected root. Asserting what we meant to write would only restate the line that writes it. * The smoke job reads the shipped entry names BEFORE `Expand-Archive`. Order is the whole claim. * `windows-archive-smoke` now checks out the tree and runs the repository's own `install.ps1` against the archive this run just built (`-BaseUrl` takes a directory), then RUNS each installed binary and matches `--version` against the tag. This is the step whose absence let it ship three times. * `crates/indexer/tests/windows_archive_shape.rs` moves all of it to `cargo test`. All four mutations run red. FOUR GATES CAUGHT MY OWN CHANGE, and each was right: * `a_powershell_block_carries_no_byte_windows_powershell_would_misdecode` -- I put U+2500 box-drawing characters in a PowerShell `run:` block. 5.1 reads a BOM-less script as cp1252. The same family of bug as the one being fixed. * `every_workflow_job_measures_free_disk_before_it_builds` -- adding a checkout put the smoke job in scope, and that rule keeps no exemption list on purpose. * `every_file_that_names_this_release_is_a_declared_site` -- two of my comments named the live version, making them version sites. * rustfmt. And three errors of my own in the new gate, all one shape -- reading prose as structure: it matched `Expand-Archive` in its own explanatory comment, its job-boundary scan stopped at a two-space comment ending in a colon, and `contains("OpenRead")` was satisfied by a mutation that left the word in a comment. Every assertion there now requires a line that RUNS. Separately, three steps share the name `Package release archive`, so the first draft graded the LINUX `tar` step. `version_bump.sh` also called a complete bump INCOMPLETE: `rusqlite` sits at 0.31.0, so after all thirteen of our crates had moved to 0.31.1 the lockfile still contained the string. Lockfiles are now exempt from that half only -- they must still state the NEW version. Verified by a round-trip bump in a copy of the tree. Gates, green on one unchanged tree: fmt, clippy -D warnings, rustdoc -D warnings, 374 workspace suites / 4008 tests, 73 e2e suites / 840 tests on the daemon leg, guest gates under --locked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126PDDLB4wNHxKXvWM1VNmu