Skip to content

fix(scripts/win-installer): install the version reported in Add/Remove Programs - #27927

Draft
david-fraley wants to merge 4 commits into
mainfrom
dfraley/ent-137-windows-installer-overwrite
Draft

fix(scripts/win-installer): install the version reported in Add/Remove Programs#27927
david-fraley wants to merge 4 commits into
mainfrom
dfraley/ent-137-windows-installer-overwrite

Conversation

@david-fraley

@david-fraley david-fraley commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Installing a newer Coder release over an older one can leave coder.exe untouched while Add/Remove Programs is updated to the new version. The reporter installed v2.35.3 over v2.34.7 and got ARP 2.35.3.0 alongside coder version reporting v2.34.7+b7d2742.

Root cause

The install section used SetOverwrite ifnewer, which compares file mtimes, while the registry writes a few lines below are unconditional. Coder cuts patch releases for older minor branches after newer minors ship, so the older release routinely carries the newer binary. Read straight out of the two published installers with 7z l:

Released installer embedded bin/coder.exe mtime
v2.35.3 (newer release) 2026-07-27 09:08:34
v2.34.7 (older release) 2026-07-28 14:58:26

So upgrading v2.34.7 to v2.35.3 asks NSIS to replace a file that is a day newer than its payload, ifnewer skips it, and the registry still advertises 2.35.3. SetOverwrite on (the NSIS default) restores the invariant that the installer installs what it contains. Downgrades and repair installs over a locally modified binary fail the same way and are fixed by the same change.

Fixes #27088
Fixes #27876
Linear: ENT-137, PLAT-466

Scope

This kills a cause that fires unconditionally: with the shipped artifacts, every v2.34.7 to v2.35.3 upgrade hits it, with no dependence on user action or timing. It does not close the whole symptom class. Two other paths produce the same "ARP disagrees with the CLI" result and are untouched here:

  • Locked coder.exe. If the binary is running, Windows cannot replace it. Verified under Wine that the silent installer exits 0 and keeps the old binary both before and after this change, so ARP still ends up advertising a version that is not on disk. Closing it needs AllowSkipFiles off plus error handling around File, or rename-and-replace. That makes silent upgrades fail loudly where they currently no-op, which changes the winget and enterprise silent-deploy contract, and it is the one path this Wine harness cannot validate. It belongs in its own PR with a real Windows test.
  • PATH shadowing, unconfirmed. coder version resolves through PATH, and path.nsh only ever appends $INSTDIR\bin, so it never wins over an earlier entry. An older coder.exe from a zip or scoop install ahead of it would report the old version even after a completely successful overwrite. The reporter's screenshot shows the output of coder version, not the command with a full path, so this cannot be ruled out as a contributing factor in their case. If it turns out to be theirs, it is a separate bug and this change will not help them.

Also unchanged: the .0 suffix in ARP. DisplayVersion stays 2.35.3.0, the conventional four-part Windows form that VIProductVersion requires. Shortening it to 2.35.3 is cosmetic: winget already treats 2.35.3.0 and 2.35.3 as equal, ${CODER_VERSION} is 2.36.0-devel+<sha> on non-release builds, and existing SCCM/Intune rules matching 2.x.y.0 would see a split fleet. Not worth coupling to a correctness fix.

Verification

Real NSIS installers built by the unmodified scripts/build_windows_installer.sh from two trees, run silently under Wine. Payload mtimes are the exact values read out of the released v2.34.7 and v2.35.3 installers above.

Windows installer version mismatch before and after the fix

Scenario ARP DisplayVersion coder.exe reports
BEFORE (main) 2.35.3.0 Coder v2.34.7
AFTER (this branch) 2.35.3.0 Coder v2.35.3
How the scenario was built and run

Baseline installers come from a clean detached worktree at origin/main, fixed installers from this branch. Both trees have a byte-identical build_windows_installer.sh, so the installer.nsi diff is the only difference:

# baseline
/tmp/verify/main-wt/scripts/build_windows_installer.sh \
  --version 2.35.3 --output installer-2.35.3-baseline.exe coder-2.35.3.exe
# fixed
/home/coder/coder/scripts/build_windows_installer.sh \
  --version 2.35.3 --output installer-2.35.3-fixed.exe coder-2.35.3.exe

Payloads are stub Windows binaries that print their injected version, with mtimes set to the released installers' embedded values (2026-07-28 14:58:26 for v2.34.7, 2026-07-27 09:08:34 for v2.35.3). NSIS preserves the source mtime on extraction, so this reproduces ifnewer's exact comparison input. Each scenario uses a fresh 64-bit Wine prefix, silent-installs v2.34.7, then silent-installs v2.35.3, then reads coder.exe output and HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\Coder.

Secondary scenarios, same method:

Scenario main this branch
Downgrade v2.35.3 to v2.35.2 ARP 2.35.2.0, exe still v2.35.3 ARP 2.35.2.0, exe v2.35.2
Reinstall v2.35.3 over a locally modified newer exe exe stays v2.34.7 exe restored to v2.35.3
Upgrade while coder.exe is held open exit 0, exe stays v2.34.7 exit 0, exe stays v2.34.7

Scope of the harness: the mechanism under test is NSIS overwrite logic plus registry writes, which Wine implements faithfully, and the prefix is 64-bit so there is no WOW6432Node divergence. The locked-file row is Wine-only and not verified on Windows; Windows enforces sharing violations more strictly and would show a retry/abort dialog in an interactive install. UAC, ACL-denied targets, and the ARP UI itself are also outside what this harness can show, so a confirmation pass on real Windows before release is still worth doing.

The screenshot is hosted on the dfraley/plat-466-proof-assets branch, which exists only to serve that image and can be deleted after merge.


This pull request was created by Coder Agents on behalf of @david-fraley.

SetOverwrite ifnewer compares file timestamps, so the installer skips
coder.exe whenever the installed file is newer than the one being
installed. Coder publishes patch releases for older minors after the
newer minor ships (v2.34.7 on 2026-07-28 versus v2.35.3 on 2026-07-27),
so upgrading across those releases updated the Add/Remove Programs entry
while leaving the previous coder.exe in place. Downgrades and reinstalls
were affected the same way.

Fixes #27088
Fixes #27876
…ograms

VIProductVersion requires four numeric components, so the installer pads
the release version with a trailing ".0" and wrote that padded value to
the Add/Remove Programs DisplayVersion. Windows then advertised 2.35.3.0
for a release the CLI reports as v2.35.3. Write the unpadded release
version instead and keep the padded one for the version resource.
@linear-code

linear-code Bot commented Aug 6, 2026

Copy link
Copy Markdown

ENT-137

PLAT-466

Drop the sentence restating what SetOverwrite on does and keep only the
non-obvious reason the previous ifnewer setting skipped coder.exe.
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.

Version mismatch for v2.35.3 Upgrade from v2.33.11 to v2.34.5 does not update executable version, while v2.33.10 → v2.34.5 works as expected

1 participant