From f1a1c270945d0a553b437f2f0ed757e4c0c3d2d4 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Tue, 21 Jul 2026 17:12:48 -0400 Subject: [PATCH 1/3] Fix MSI installer to always replace any existing version Installing SysML2Workbench previously only replaced strictly-older versions and could leave two side-by-side Add/Remove Programs entries when a prerelease and its stable release computed to the identical MSI ProductVersion (build.yaml truncated the SemVer prerelease suffix naively). Fixes both problems: - build.yaml: compute the MSI build field as patch*1000 + prerelease-number, reserving 999 for stable releases, so every prerelease/patch/stable combination gets a distinct, monotonically increasing MSI version and never collides. - Package.wxs: MajorUpgrade now uses AllowDowngrades="yes" so installing SysML2Workbench always replaces whatever version is already present - older, same, or newer - rather than ever refusing to install. - Installer.wixproj: suppress ICE61, a known false positive for AllowDowngrades (it expects an Upgrade table Maximum version, which AllowDowngrades intentionally omits). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yaml | 39 ++++++++++++++++++- ...nsulting.SysML2Workbench.Installer.wixproj | 6 +++ .../Package.wxs | 8 +++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4992232..4e4ed33 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -234,9 +234,44 @@ jobs: if: matrix.os == 'windows-latest' shell: bash run: | - MSI_VERSION=$(echo "${{ inputs.version }}" | cut -d'-' -f1) + # MSI ProductVersion only supports three numeric fields (major.minor.build, each + # 0-65535) and cannot represent a SemVer prerelease suffix (e.g. "-beta.12"). + # Naively truncating at the first '-' collapses every prerelease of a version + # (and its eventual stable release) onto the identical MSI version number, so + # Windows Installer cannot tell a newer prerelease/stable build apart from an + # older one already installed - MajorUpgrade then leaves the old install in + # place and adds a second, independent Add/Remove Programs entry instead of + # replacing it. Encode patch+prerelease into the build field instead: + # build = patch*1000 + prerelease-number, reserving 999 for stable (non- + # prerelease) releases so a stable release always sorts above every prerelease + # of the same patch. + FULL_VERSION="${{ inputs.version }}" + NUMERIC_PART="${FULL_VERSION%%-*}" + MAJOR=$(echo "$NUMERIC_PART" | cut -d'.' -f1) + MINOR=$(echo "$NUMERIC_PART" | cut -d'.' -f2) + PATCH=$(echo "$NUMERIC_PART" | cut -d'.' -f3) + + if [ "$FULL_VERSION" = "$NUMERIC_PART" ]; then + # Stable release - reserve the top of this patch's build range. + PRERELEASE_NUM=999 + else + PRERELEASE_PART="${FULL_VERSION#*-}" + PRERELEASE_NUM=$(echo "$PRERELEASE_PART" | grep -oE '[0-9]+$') + if [ -z "$PRERELEASE_NUM" ] || [ "$PRERELEASE_NUM" -ge 999 ]; then + echo "Error: prerelease number in '${FULL_VERSION}' must be a non-negative integer less than 999" >&2 + exit 1 + fi + fi + + BUILD=$((PATCH * 1000 + PRERELEASE_NUM)) + if [ "$BUILD" -gt 65535 ]; then + echo "Error: computed MSI build number ${BUILD} exceeds the 65535 field limit" >&2 + exit 1 + fi + + MSI_VERSION="${MAJOR}.${MINOR}.${BUILD}" echo "MSI_VERSION=${MSI_VERSION}" >> "$GITHUB_ENV" - echo "Computed MSI version: ${MSI_VERSION}" + echo "Computed MSI version: ${MSI_VERSION} (from ${FULL_VERSION})" - name: Build MSI installer if: matrix.os == 'windows-latest' diff --git a/src/DemaConsulting.SysML2Workbench.Installer/DemaConsulting.SysML2Workbench.Installer.wixproj b/src/DemaConsulting.SysML2Workbench.Installer/DemaConsulting.SysML2Workbench.Installer.wixproj index ed75346..e6811d4 100644 --- a/src/DemaConsulting.SysML2Workbench.Installer/DemaConsulting.SysML2Workbench.Installer.wixproj +++ b/src/DemaConsulting.SysML2Workbench.Installer/DemaConsulting.SysML2Workbench.Installer.wixproj @@ -23,6 +23,12 @@ Version=$(Version);SysML2WorkbenchPublishDir=$(SysML2WorkbenchPublishDir);SysML2WorkbenchIconFile=$(SysML2WorkbenchIconFile) + + ICE61 diff --git a/src/DemaConsulting.SysML2Workbench.Installer/Package.wxs b/src/DemaConsulting.SysML2Workbench.Installer/Package.wxs index f29a34a..32c54d2 100644 --- a/src/DemaConsulting.SysML2Workbench.Installer/Package.wxs +++ b/src/DemaConsulting.SysML2Workbench.Installer/Package.wxs @@ -9,7 +9,13 @@ Compressed="yes" Scope="perMachine"> - + + From ebaf8b5b0bad8d90fb33f0947d1ba8a641a48665 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Tue, 21 Jul 2026 17:23:12 -0400 Subject: [PATCH 2/3] Close running app on upgrade, tidy up Add/Remove Programs entry - Package.wxs: add util:CloseApplication so a running SysML2 Workbench is closed (gracefully, then forcibly after a short timeout) before install/upgrade replaces its files, instead of the upgrade failing or silently leaving stale files in use. - Package.wxs: set ARPHELPLINK/ARPURLINFOABOUT to the project's GitHub page and ARPCONTACT to DEMA Consulting, so Add/Remove Programs shows useful support/about links instead of nothing. - Package.wxs: set ARPNOMODIFY/ARPNOREPAIR since there is no WixUI dialog sequence to drive those buttons - hide them rather than offer non-functional options. - Installer.wixproj: reference WixToolset.Util.wixext for the CloseApplication element. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...nsulting.SysML2Workbench.Installer.wixproj | 6 +++++ .../Package.wxs | 26 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/DemaConsulting.SysML2Workbench.Installer/DemaConsulting.SysML2Workbench.Installer.wixproj b/src/DemaConsulting.SysML2Workbench.Installer/DemaConsulting.SysML2Workbench.Installer.wixproj index e6811d4..e35a76d 100644 --- a/src/DemaConsulting.SysML2Workbench.Installer/DemaConsulting.SysML2Workbench.Installer.wixproj +++ b/src/DemaConsulting.SysML2Workbench.Installer/DemaConsulting.SysML2Workbench.Installer.wixproj @@ -31,4 +31,10 @@ ICE61 + + + + + diff --git a/src/DemaConsulting.SysML2Workbench.Installer/Package.wxs b/src/DemaConsulting.SysML2Workbench.Installer/Package.wxs index 32c54d2..4312be8 100644 --- a/src/DemaConsulting.SysML2Workbench.Installer/Package.wxs +++ b/src/DemaConsulting.SysML2Workbench.Installer/Package.wxs @@ -1,5 +1,6 @@ - + + + + + + + + + + + + + From 3221518820c9b9228b7a16a3a43d27d8dff08773 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Tue, 21 Jul 2026 17:39:44 -0400 Subject: [PATCH 3/3] Simplify MSI version computation back to naive truncation The patch*1000+prerelease-number/999-sentinel encoding was solving a version-collision bug that AllowDowngrades="yes" (added in the previous commit) already makes irrelevant - that setting replaces whatever version is installed regardless of number comparison, so two builds truncating to the same MSI version is no longer a problem. Worse, the encoding produced nonsensical Add/Remove Programs version numbers (e.g. stable 0.1.1 would show as 0.1.1999), so it's not worth keeping even for display purposes. Reverting to the simple truncation that was there before. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yaml | 43 ++++++------------------------------ 1 file changed, 7 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4e4ed33..2d83545 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -235,43 +235,14 @@ jobs: shell: bash run: | # MSI ProductVersion only supports three numeric fields (major.minor.build, each - # 0-65535) and cannot represent a SemVer prerelease suffix (e.g. "-beta.12"). - # Naively truncating at the first '-' collapses every prerelease of a version - # (and its eventual stable release) onto the identical MSI version number, so - # Windows Installer cannot tell a newer prerelease/stable build apart from an - # older one already installed - MajorUpgrade then leaves the old install in - # place and adds a second, independent Add/Remove Programs entry instead of - # replacing it. Encode patch+prerelease into the build field instead: - # build = patch*1000 + prerelease-number, reserving 999 for stable (non- - # prerelease) releases so a stable release always sorts above every prerelease - # of the same patch. - FULL_VERSION="${{ inputs.version }}" - NUMERIC_PART="${FULL_VERSION%%-*}" - MAJOR=$(echo "$NUMERIC_PART" | cut -d'.' -f1) - MINOR=$(echo "$NUMERIC_PART" | cut -d'.' -f2) - PATCH=$(echo "$NUMERIC_PART" | cut -d'.' -f3) - - if [ "$FULL_VERSION" = "$NUMERIC_PART" ]; then - # Stable release - reserve the top of this patch's build range. - PRERELEASE_NUM=999 - else - PRERELEASE_PART="${FULL_VERSION#*-}" - PRERELEASE_NUM=$(echo "$PRERELEASE_PART" | grep -oE '[0-9]+$') - if [ -z "$PRERELEASE_NUM" ] || [ "$PRERELEASE_NUM" -ge 999 ]; then - echo "Error: prerelease number in '${FULL_VERSION}' must be a non-negative integer less than 999" >&2 - exit 1 - fi - fi - - BUILD=$((PATCH * 1000 + PRERELEASE_NUM)) - if [ "$BUILD" -gt 65535 ]; then - echo "Error: computed MSI build number ${BUILD} exceeds the 65535 field limit" >&2 - exit 1 - fi - - MSI_VERSION="${MAJOR}.${MINOR}.${BUILD}" + # 0-65535) and cannot represent a SemVer prerelease suffix (e.g. "-beta.12"), so + # it's simply truncated off here. Package.wxs's MajorUpgrade AllowDowngrades="yes" + # is what makes this safe: it always replaces whatever version is installed - + # older, same, or newer - so two builds truncating to the same MSI version (e.g. + # a prerelease and its eventual stable release) is not a problem. + MSI_VERSION=$(echo "${{ inputs.version }}" | cut -d'-' -f1) echo "MSI_VERSION=${MSI_VERSION}" >> "$GITHUB_ENV" - echo "Computed MSI version: ${MSI_VERSION} (from ${FULL_VERSION})" + echo "Computed MSI version: ${MSI_VERSION}" - name: Build MSI installer if: matrix.os == 'windows-latest'