Skip to content

fix(soak): match .exe on windows tool-install early-exit - #1028

Merged
jdx merged 1 commit into
aubepkg:mainfrom
jdalton:fix/win-tool-install-exe-idempotency
Jul 13, 2026
Merged

jdx merged 1 commit into
aubepkg:mainfrom
jdalton:fix/win-tool-install-exe-idempotency

Conversation

@jdalton

@jdalton jdalton commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

What

Follow-up to #1020. The pre-download idempotency guard in installAssetTool (scripts/soak/external-tools.mts) checked only the suffix-less destBin, while the post-extract path resolves <bin>.exe for Windows archives. So on Windows a second aube dev-tool --install always re-downloaded and re-extracted the archive — and on a restricted/offline runner (cached rack, no network) that forced re-download fails outright.

This mirrors the same .exe resolution into the early-exit block, so a repeat install is a no-op on Windows as it already is on POSIX.

Why

Resolves the remaining unresolved greptile P1 on #1020 ("Idempotency check misses .exe on Windows", external-tools.mts:288). The sibling P1 (".zip handle name doubles the .exe extension") was already handled by the merged linkHandle, which strips the suffix before building the PATH handle.

Testing

  • node --test scripts/soak/external-tools.test.mts — 13/13 pass
  • node scripts/soak/external-tools.mts --check — clean

A dedicated unit test isn't added: RACK_DIR/BIN_DIR are frozen from XDG_DATA_HOME at import and installAssetTool isn't exported, so exercising the guard would need a subprocess + env-override harness disproportionate to a 4-line Windows-only mirror of already-present logic (lines 296–299).

Note: scripts/soak/ is mirrored into the nub repo (only paths.mts differs); this one-liner should be mirrored there too.

This PR was generated by Claude.

Summary by CodeRabbit

  • Bug Fixes
    • Improved Windows installation handling for executable archives.
    • Prevented unnecessary re-downloads and installation retries when an executable was already installed with an .exe extension.

The post-extract path resolves <bin>.exe for windows archives, but the
pre-download idempotency guard checked only the suffix-less destBin, so a
second --install on windows always re-downloaded and re-extracted (and
wedges on a restricted/offline runner). Mirror the same .exe resolution
in the early-exit. Resolves the remaining greptile P1 from aubepkg#1020.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The asset-install flow now detects an existing Windows .exe artifact before the installed check and link creation, preventing unnecessary repeated downloads.

Changes

Windows asset installation

Layer / File(s) Summary
Pre-install .exe resolution
scripts/soak/external-tools.mts
installAssetTool updates destBin and binName when only the .exe executable exists, allowing repeated installs to short-circuit and link the correct target.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Poem

A bunny found an .exe in the hay,
So downloads no longer hopped away.
The link points right, the check succeeds,
No repeat fetches among the trees.
“Windows is tidy!” the rabbit sings.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the Windows .exe early-exit fix in the soak tool-install flow.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/soak/external-tools.mts`:
- Around line 284-290: Restrict the `.exe` fallback in the installer’s
destination resolution to `process.platform === 'win32'`, so non-Windows
platforms never reuse or rename an adjacent executable. Apply the same Windows
guard to the post-extraction check around the corresponding destination and
command-name updates, keeping first and repeated installs consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 5173ddb2-d600-4da3-adb5-e92ebe061cbd

📥 Commits

Reviewing files that changed from the base of the PR and between e23dd6a and a21fcf1.

📒 Files selected for processing (1)
  • scripts/soak/external-tools.mts

Comment on lines +284 to +290
// Windows archives land as `<bin>.exe`; resolve the same suffix the
// post-extract path does so a second --install is a no-op instead of a
// forced re-download (which wedges on a restricted/offline runner).
if (!existsSync(destBin) && existsSync(`${destBin}.exe`)) {
destBin = `${destBin}.exe`
binName = `${binName}.exe`
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restrict .exe resolution to Windows.

This branch is described as Windows-specific, but it currently runs on every platform. If a non-Windows installation has <destBin>.exe alongside the expected binary, the installer will reuse it and change the linked command name unexpectedly. Add process.platform === 'win32' here and apply the same guard to the post-extraction check at Lines 301-306 (or extract both checks into one helper) to keep first and repeated installs consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/soak/external-tools.mts` around lines 284 - 290, Restrict the `.exe`
fallback in the installer’s destination resolution to `process.platform ===
'win32'`, so non-Windows platforms never reuse or rename an adjacent executable.
Apply the same Windows guard to the post-extraction check around the
corresponding destination and command-name updates, keeping first and repeated
installs consistent.

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a Windows-only idempotency regression in installAssetTool: the early-exit guard checked for destBin (no suffix), but Windows extraction produces <bin>.exe, so a second --install always fell through to re-download on Windows.

  • Mirrors the identical .exe probe (!existsSync(destBin) && existsSync(destBin + '.exe')) already present in the post-extract block (lines 303–306) into the entry guard, so both destBin and binName are resolved before the existsSync check that gates the early return.
  • The linkHandle call on the early-exit path receives the correct .exe-suffixed binName, and linkHandle already strips .exe via name.replace(/\\.exe$/, '') on Windows before writing the shim, so no double-extension issue arises.

Confidence Score: 5/5

Safe to merge — a 7-line addition that mirrors existing, already-tested logic to the early-exit block, with no new behavioral paths introduced for POSIX.

The new block is a direct structural copy of the post-extract .exe probe (lines 303–306). linkHandle already handles .exe-suffixed binName values correctly via its name.replace(/\.exe$/, '') normalisation. The change is Windows-only in practice, is covered by the existing test suite (13/13 pass), and introduces no new code paths for POSIX runners.

No files require special attention.

Important Files Changed

Filename Overview
scripts/soak/external-tools.mts Mirrors the existing post-extract .exe resolution (lines 303-306) into the early-exit idempotency block, making repeat installs on Windows a no-op

Reviews (1): Last reviewed commit: "fix(soak): match .exe on windows tool-in..." | Re-trigger Greptile

@jdx
jdx merged commit 448286c into aubepkg:main Jul 13, 2026
17 checks passed
@jdalton

jdalton commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

@jdx You're merging quick, I have another follow up but related to robustness (updating a lib)

@jdalton
jdalton deleted the fix/win-tool-install-exe-idempotency branch July 13, 2026 22:50
jdalton added a commit to jdalton/nub that referenced this pull request Jul 15, 2026
The post-extract path resolves <bin>.exe for windows archives, but the
pre-download idempotency guard checked only the suffix-less destBin, so a
second --install on windows re-downloaded and re-extracted (and wedges on
a restricted/offline runner). Mirror the same .exe resolution in the
early-exit. Mirrors aubepkg/aube#1028.
colinhacks added a commit to nubjs/nub that referenced this pull request Jul 24, 2026
…d tooling (#442)

* build: pin rust 1.95.0 toolchain, bump rust-version to 1.95

Exact stable pin via rust-toolchain.toml so all four workspaces (nub,
nub-native, nub-phantom, vendor/aube) build with one toolchain. 1.95
is the floor for core::hint::cold_path().

* perf(pm): mark rare branches in hot install loops with cold_path()

core::hint::cold_path() (stable since rust 1.95) annotates the rare
arm so the optimizer sinks it off the hot path. Sites: the per-file
materialize loops' reflink/hardlink fallbacks and link-error returns
(project-local and GVS staging), the pnpm-lock byte-cursor subset
parser's decline-to-serde bails, the thread-local semver range and
version cache misses, and the per-entry tar walk's validation
rejects. No behavior change; hints only.

* build: switch toolchain pin to nightly-2026-07-04 (7-day soak)

Adopt the newest nightly at least 7 days old as of 2026-07-11; bump
the pin under the same soak rule. rust-version stays 1.95 as the
stable msrv floor. Verified on nightly-2026-07-04: clippy
--all-targets -D warnings clean across nub and vendor/aube.

* build: enable cargo min-publish-age with a 7-day soak

Crate versions published less than 7 days ago are pubtime-incompatible
and the resolver skips them unless already in Cargo.lock. Unstable
cargo feature, active via the nightly pinned in rust-toolchain.toml;
stable cargo ignores the [unstable] table. Unignore the root
.cargo/config.toml the same way as the nub-native one.

* build: add 7-day soak manager, taze, pinned external tools

scripts/soak/constants.mts is the one source for the release-age
window; soak.mts parity-checks and fixes every surface (.cargo
min-publish-age, pnpm-workspace.yaml minimumReleaseAge in minutes,
.npmrc min-release-age in days, taze maturityPeriod via direct
import) and enforces lockstep between catalog entries and
catalog-shadowed exact pins. Version-pinned soak excludes require a
'# published: | removable:' date annotation and expired pins are
pruned by --fix. update-deps.mts bumps npm (taze) and cargo through
the same window, refusing a non-rustup cargo that would silently
skip the [unstable] soak. external-tools.json pins external security
tools exact with sha512 SRI; external-tools.mts is the only install
path; paths.mts is the single declaration of every location the
scripts touch. taze is exact-pinned in package.json (npm cannot parse
the catalog: protocol and wpt-worker runs a root npm ci) with the
catalog entry as the enforced reference.

* ci: gate soak + tool pins, route installs through socket firewall, add zizmor

docs-links runs soak.mts --check and external-tools.mts --check; the
test job installs sfw and its package-manager shims before pnpm
install. The SOCKET_SECURITY_KEY repo secret (not yet set) feeds
SOCKET_API_KEY + SOCKET_API_TOKEN; until it lands sfw runs its
keyless free tier. zizmor.yml lints workflow files on PRs.

* fix(soak): make update-deps dry-run actually dry

The taze config sets write: true, so a dry run must pass --no-write
explicitly or it rewrites package.json; also suppress the ni install
prompt with --no-install.

* fix(soak): timeout stalled external-tool downloads

A hung release or registry response would stall CI indefinitely;
abort after 120s (generous for the largest pinned binary on a slow
runner).

* fix(soak): harden gates and installer against review findings

- reject flow-style minimumReleaseAgeExclude (invisible to the block
  parser = unvalidated bypass) and parse entries with trailing
  comments; catalog parity tolerates blank lines in the block
- entrypoint guard uses realpath + pathToFileURL so symlinked or
  space-containing checkouts don't silently no-op the gate
- linkHandle force-removes dangling handles (EEXIST wedge)
- extract .zip release assets via bsdtar and verify the binary landed
- GITHUB_TOKEN only sent to github.com, never the npm registry
- npm-purl installs key completion off the extracted manifest so an
  interrupted extract self-heals
- update-deps runs every requested ecosystem and reports spawn errors
- sfw install/shims skip cleanly on windows (POSIX-only mechanism)
- Socket credential is SOCKET_SECURITY_KEY everywhere — no
  SOCKET_API_* aliasing in CI env or the enterprise-tier check

* build: manage .dockerignore with untracked, gate docker prebake pins

untracked 1.6.4 (catalog-shadowed pin, lockstep-enforced) maintains a
generated exclusion section in .dockerignore between markers
(pnpm run dockerignore:update), shrinking the docker build context
for the from-source images; the package.json whitelist rescues the
load-bearing .cargo configs (crates/nub-native/.cargo routes
in-container build output to the shared target dir). external-tools
--check gains a docker-prebake parity gate (null surface here — nub's
docker images are product artifacts, not dev-tool prebakes).

* ci: zizmor hygiene across all workflows

267 findings to zero at the default persona: persist-credentials:
false on every checkout that doesn't push (the homebrew-tap bump
keeps its credentialed checkout, inline-ignored with the reason);
workflow-level permissions: {} with per-job scopes matched to what
each job's steps actually use; all template-injection sites moved to
env-var indirection (28 in release.yml alone, zero behavior change);
adhoc package installs documented as already env-pinned; deliberate
residuals carry inline zizmor ignores with per-site verified reasons
(cache-poisoning on gate-only jobs, secrets-inherit where docker.yml
declares no workflow_call secrets).

* build: pin pnpm 11.8 + npm 12 via the tool rack, dogfood dep installs

Same mechanism as the sibling repos: SRI-pinned pnpm/npm in
external-tools.json (npm 12.0.0 carries a dated soakBypass
annotation), CI installs both before the firewall shims, shims wrap
the pinned binaries directly, and npm-packaged external tools
materialize runtime deps through nub itself first (pnpm/npm
fallbacks), prod-only with scripts skipped. Also hardens two skill
docs flagged by static skill scanning: the vendor-sync rsync
--delete now guards against an empty staging tree, and the bench
cache-wipe recipe fails loudly on an empty cache-path substitution.

* fix(soak): shim self-clobber, windows .exe resolve, date guards; add unit tests

writeShims wrote THROUGH the rack symlink handle, overwriting the
pinned binary with the shim body — which then exec'd itself forever.
Remove the handle before writing. Windows .zip archives resolve
<bin>.exe before failing, and a failed extract cleans its dir so
retries re-extract. Annotation and soakBypass dates must be real
calendar dates (finding, not a RangeError crash), and an expired
soakBypass is flagged like any other soaked pin. node:test unit
tests cover the checkers, fixers, and parsers (pnpm run
test:scripts; also in the CI gate).

* refactor(soak): perfectionist pass — dead code out, tests isolated, docker gates wired

Mirrors the shared-script pass (dead assertRepoRoot/'required' knob
out, hasSocketToken inlined, shared extractArchive, single manifest
parse, manifest-resolved shim targets with idempotence + clobber
regression tests, tmpdir-isolated rack tests, SOAK_DAYS-derived
fixtures, SOCKET_SECURITY_KEY prose). nub-side: .dockerignore joins
the docker-smoke and docker workflow path filters so context changes
actually build the images; new dockerignore tests assert the managed
section, preserved custom rules, honored whitelist, and that no
recursive dotfile rule can reach the load-bearing nested .cargo
config; CI gate defers to the package.json scripts; zizmor.yml uses
the fleet checkout pin; ignore-comment and permissions placement
normalized (zizmor still reports zero findings); package.json em
dashes restored to literals

* feat(soak): enforce the toolchain nightly soak, add the soak skill

rust-toolchain.toml carries a machine-read '# adopted: YYYY-MM-DD'
line and soak.mts enforces adopted >= nightly date + SOAK_DAYS as a
fifth checked surface (stable pins pass freely; missing or impossible
dates are findings). .claude/skills/soak/SKILL.md documents the
system pointing at scripts/soak/: the one-place window change (edit
SOAK_DAYS, run soak:fix), the committed-only opt-out (SOAK_DAYS = 0
— deliberately no env bypass), per-package dated excludes, per-tool
soakBypass, and the nightly bump procedure.

* docs(soak): align skill with claude prompting and skill-authoring guidance

* test(soak): cover cli paths, fixers, download token gating, installer guards

* refactor(soak): dedupe the rust-toolchain.toml path, complete the header inventory

* fix(ci): fixture anchor, zizmor scope, docker whitelist, sfw cargo bypass

- tests/node-matrix/fixtures/package.json: workspaces:[] boundary so the
  new root pnpm-workspace.yaml + root engines can't redirect matrix legs
  to the highest installed node (proved: node 20 leg ran 26.4 without it)
- zizmor.yml: scan .github/ only — vendor/aube ships aube's workflow
  files, which GitHub never executes outside the root .github/
- .dockerignore: whitelist tests/docker-smoke (untracked's test* pattern
  excluded the smoke.sh the docker-smoke Dockerfiles COPY)
- ci.yml: run the .mts soak scripts with a type-stripping node on compat
  legs, pin pnpm/npm only on the node-24 leg, and exec real cargo past
  the sfw shim (the proxy mangles the registry-mock tests: ECONNRESET)

* fix(soak): anchor the pnpm soak in tools/ — a root workspace yaml redirects the test matrix

pnpm only honors minimumReleaseAge from a workspace yaml (.npmrc's
minimum-release-age is silently ignored — proven: fresh npm@12.0.0
resolves through npmrc but is blocked by the yaml with
ERR_PNPM_NO_MATURE_MATCHING_VERSION). But a workspace yaml beside the
pre-existing root pnpm-lock.yaml marks the REPO ROOT as a workspace for
nub itself: every in-repo invocation then inherits the root package.json
engines (>=22.15) and redirects to the highest installed node, which
un-skipped the tier-test harness gates and failed the 18.19/20.11/22.13/
22.15 matrix legs (main nightly green; branch bisected to the yaml).

Same shape as aube's docs/ anchor: the root is npm-only (npm ci,
package-lock.json, .npmrc min-release-age; root pnpm-lock deleted and
gitignored), and the pnpm domain — catalog, minimumReleaseAge, taze —
lives in tools/ where the workspace boundary sits below anything the
tests walk. Verified: the three failing integration tests pass under a
Node 20.11 PATH again, and all soak gates stay green.

* fix(ci): reachable rust-toolchain pin, windows-safe tar extraction

- dtolnay/rust-toolchain@29eef336 is a real upstream commit but
  DANGLING: dtolnay force-pushes the stable branch each release, so the
  old head is unreachable from any ref and the new zizmor gate's online
  impostor-commit audit rejects it (24 sites). Repin to the current
  stable head 4be7066a.
- tar on Windows parses an absolute C:\ archive path as a remote
  host:path ("Cannot connect to C") — extract with a cwd-relative
  archive name, which unblocks the pinned-pnpm install on the
  windows-latest leg

* ci: exact version comments on docker action hash pins (ref-version-mismatch)

* fix(soak): use System32 bsdtar on windows — Git Bash's GNU tar can't read zip

* fix(soak): windows handles forward to the rack — symlinks strand SEA siblings

pnpm's Windows SEA launcher resolves dist/pnpm.mjs relative to the
invoked handle's directory, so a symlinked/copied bin handle crashes
with MODULE_NOT_FOUND. On win32 the handle is now a forwarding pair
(.cmd for cmd/pwsh + extensionless bash shim for Git Bash) that invokes
the absolute rack target; registry-tarball tools forward through node
directly instead of a bash-only wrapper pwsh can't run.

* fix(deps): strip .exe from win32 tool handle base name

* chore: encode PR home + push fork as a checked remote map

* fix(deps): close the renovate soak bypass — explicit minimumReleaseAge, parity-checked

* fix: portable awk comment in commit-msg hook — C-style broke BSD awk

* test: pin commit-msg hook behavior — clean stderr, trailer strip, blank collapse

* fix(soak): match .exe on windows tool-install early-exit

The post-extract path resolves <bin>.exe for windows archives, but the
pre-download idempotency guard checked only the suffix-less destBin, so a
second --install on windows re-downloaded and re-extracted (and wedges on
a restricted/offline runner). Mirror the same .exe resolution in the
early-exit. Mirrors aubepkg/aube#1028.

* build: drop the repo-root nightly pin, soak cargo at update time

A repo-root rust-toolchain.toml outranks `rustup default` in rustup's
toolchain precedence, and dtolnay/rust-toolchain only sets the default.
Pinning nightly there therefore redirected every version-pinned CI job --
including the MSRV Check legs that compile on 1.93.0 and 1.95.0 -- and
built the released binaries on nightly rather than stable.

min-publish-age is the only thing that needed nightly, and it only
matters where versions are chosen. Request it per invocation there:
update-deps.mts now runs `cargo +nightly update`. Every CI job and every
shipped binary builds on stable again, and the MSRV legs verify their
declared floors.

Drops the now-unreachable toolchain-soak checker and its tests; the
remaining surfaces are unchanged.

* ci: clear zizmor workflow-hygiene findings

- Correct the actions/checkout@93cb6efe pin comments from the moving major
  tag "v5" to the exact immutable tag v5.0.1 the SHA resolves to (comment
  only; no SHA changed). Clears 53 ref-version-mismatch findings.
- Suppress impostor-commit and ref-version-mismatch on the dtolnay/rust-toolchain
  pins; the repo force-pushes its version tags, so pinned SHAs go unreachable
  and cannot be pinned to a stable tag. Both SHAs verified as genuine upstream
  commits.
- Add persist-credentials: false to the two verify-install.yml checkout steps
  flagged by artipacked; neither job pushes to the repo.
- Suppress dangerous-triggers on trunk-red.yml; it is a report-only workflow_run
  notifier that never checks out or runs PR-controlled code.

---------

Co-authored-by: Colin McDonnell <3084745+colinhacks@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants