docs/github-actions-windows-wix-research.md
Research Date: 2025-11-13 Target: Building WiX Toolset 4.x installers for Go applications on GitHub Actions
windows-latest (Windows Server 2022)Important Migration Note: Starting September 2, 2025, windows-latest will migrate from Windows Server 2022 to Windows Server 2025, rolling out progressively until September 30, 2025.
Pre-installed .NET Core SDK versions:
Note: Historical versions included .NET 6.x and 7.x, but these are being phased out. Always check the official Windows2022-Readme.md for current versions.
PowerShell Version: 7.4.13
Pre-installed PowerShell Modules:
Visual Studio Enterprise 2022:
C:\Program Files\Microsoft Visual Studio\2022\EnterpriseMSBuild:
Windows SDK Versions:
Method 1: Workflow Logs
- name: Show runner image
run: |
echo "Runner Image: $env:ImageVersion"
Get-ChildItem Env:
Check the "Set up job" section in your workflow logs, then expand the "Runner Image" section. The "Included Software" link shows all pre-installed tools.
Method 2: Official Repository
Installation:
- name: Install WiX Toolset
run: dotnet tool install --global wix
Requirements:
windows-latest)Building:
- name: Build MSI
run: wix build -o output/installer.msi Product.wxs
With Build Variables:
- name: Build MSI with version
run: wix build Product.wxs -o output/installer-${{ github.ref_name }}.msi -d BuildVersion=${{ github.ref_name }}
Pros:
Cons:
Installation:
- name: Install WiX via Chocolatey
run: choco install wixtoolset -y
Usage:
- name: Add WiX to PATH
run: echo "C:\Program Files (x86)\WiX Toolset v3.11\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Build MSI
run: |
candle.exe Product.wxs -out obj/Product.wixobj
light.exe obj/Product.wixobj -out bin/Installer.msi
Pros:
Cons:
Project File Setup (.wixproj):
<Project Sdk="WixToolset.Sdk/4.0.0">
<PropertyGroup>
<OutputName>MyInstaller</OutputName>
<OutputType>Package</OutputType>
</PropertyGroup>
<ItemGroup>
<Compile Include="Product.wxs" />
</ItemGroup>
</Project>
Workflow:
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v1
- name: Install WiX
run: dotnet tool install --global wix
- name: Build with MSBuild
run: msbuild MyInstaller.wixproj /p:Configuration=Release
Pros:
Cons:
| Method | Installation Time | WiX Version | Complexity | Recommended |
|---|---|---|---|---|
| .NET Tool | < 10 seconds | 4.x | Low | Yes |
| Chocolatey | 30-60 seconds | 3.x | Medium | No (legacy) |
| MSBuild | 10-20 seconds | 4.x | Medium-High | For .NET projects |
Storage Quotas:
Individual Artifact Limits:
Job Limits:
Default Retention:
Custom Retention:
- name: Upload MSI artifact
uses: actions/upload-artifact@v4
with:
name: installer-msi
path: output/*.msi
retention-days: 30 # Override default
Important Notes:
Default Compression:
Custom Compression:
- name: Upload large MSI with minimal compression
uses: actions/upload-artifact@v4
with:
name: installer-msi
path: output/*.msi
compression-level: 0 # 0-9 range
Compression Guidelines:
For MSI files (already compressed), use compression-level: 0 for significantly faster uploads.
Best Practices:
# Bad: Generic names
- name: artifact
path: output/*.msi
# Good: Descriptive names with metadata
- name: mcpproxy-installer-windows-amd64-${{ github.ref_name }}
path: output/mcpproxy-${{ github.ref_name }}-amd64.msi
# Release vs Prerelease
- name: mcpproxy-installer-windows-amd64-prerelease-${{ github.sha }}
path: output/mcpproxy-*.msi
if: github.ref == 'refs/heads/next'
Recommendations:
windows-amd64, windows-arm64${{ github.ref_name }} or ${{ github.sha }}prerelease, release, nightlyImportant: In actions/upload-artifact@v4, artifacts are immutable. You cannot overwrite an existing artifact with the same name in the same job.
Solution:
# Use unique names per build
- name: mcpproxy-installer-windows-amd64-${{ github.run_number }}
Average Times:
Drive Performance Comparison:
Optimization Technique:
- name: Install .NET SDK to D: drive
run: |
mkdir D:\dotnet
$env:DOTNET_INSTALL_DIR = "D:\dotnet"
# Install .NET or use existing from D:
Real-world Impact: Some projects report up to 86% faster .NET CI times by using D: drive.
Setup:
- name: Setup .NET with caching
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
cache: true # Enable NuGet caching
- name: Build
run: dotnet build
Requirements:
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>cache-dependency-path: '**/packages.lock.json'- name: Setup Go with caching
uses: actions/setup-go@v4
with:
go-version: '1.23'
cache: true # Automatically caches go.mod/go.sum
- name: Cache .NET tools
uses: actions/cache@v3
with:
path: ~/.dotnet/tools
key: ${{ runner.os }}-dotnet-tools-${{ hashFiles('**/dotnet-tools.json') }}
- name: Install WiX
run: dotnet tool install --global wix --version 4.0.0
Note: Global tools cache is more effective if you specify a fixed version.
- name: Cache WiX obj files
uses: actions/cache@v3
with:
path: |
installer/obj/
installer/bin/
key: ${{ runner.os }}-wix-${{ hashFiles('installer/**/*.wxs') }}
Problem: Chocolatey installs packages to various locations (C:\tools, C:\Program Files), making comprehensive caching difficult.
Verdict: Not recommended for GitHub Actions. Use .NET global tools instead.
Technique: Install common tools (PowerShell, Node.js, .NET SDK, WiX) to a virtual hard disk (VHDX on X: drive), then cache the entire VHDX file.
Setup (Advanced):
- name: Restore VHDX cache
uses: actions/cache@v3
with:
path: tools.vhdx
key: ${{ runner.os }}-vhdx-tools-v1
- name: Mount VHDX
run: |
Mount-DiskImage -ImagePath tools.vhdx
# Tools now available on X: drive
Pros: Massive performance improvement (30X faster IOPS) Cons: Complex setup, large cache size
name: Build MSI Installer
on:
push:
branches: [main, next]
pull_request:
jobs:
build-msi:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
cache: true
- name: Build Go binary
run: |
$VERSION = "${{ github.ref_name }}"
go build -o output/mcpproxy.exe ./cmd/mcpproxy
- name: Install WiX Toolset
run: dotnet tool install --global wix
- name: Build MSI
run: |
$VERSION = "${{ github.ref_name }}"
wix build installer/Product.wxs `
-o output/mcpproxy-$VERSION-amd64.msi `
-d BuildVersion=$VERSION
- name: Upload MSI artifact
uses: actions/upload-artifact@v4
with:
name: mcpproxy-installer-windows-amd64-${{ github.ref_name }}
path: output/*.msi
compression-level: 0 # MSI already compressed
retention-days: 30
name: Build MSI Installers (All Architectures)
on:
push:
tags:
- 'v*'
jobs:
build-msi:
runs-on: windows-latest
strategy:
matrix:
arch: [amd64, arm64]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET with caching
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
cache: true
- name: Setup Go with caching
uses: actions/setup-go@v4
with:
go-version: '1.23'
cache: true
- name: Cache .NET global tools
uses: actions/cache@v3
with:
path: ~/.dotnet/tools
key: ${{ runner.os }}-dotnet-tools-wix-4.0.0
- name: Build Go binary for ${{ matrix.arch }}
run: |
$env:GOOS = "windows"
$env:GOARCH = "${{ matrix.arch }}"
$VERSION = "${{ github.ref_name }}"
go build -o output/mcpproxy-${{ matrix.arch }}.exe ./cmd/mcpproxy
- name: Install WiX Toolset
run: dotnet tool install --global wix --version 4.0.0
- name: Build MSI for ${{ matrix.arch }}
run: |
$VERSION = "${{ github.ref_name }}"
$ARCH = "${{ matrix.arch }}"
wix build installer/Product.wxs `
-o output/mcpproxy-$VERSION-$ARCH.msi `
-d BuildVersion=$VERSION `
-d Platform=$ARCH `
-d BinaryPath=output/mcpproxy-$ARCH.exe
- name: Upload MSI artifact
uses: actions/upload-artifact@v4
with:
name: mcpproxy-installer-windows-${{ matrix.arch }}-${{ github.ref_name }}
path: output/mcpproxy-*.msi
compression-level: 0
retention-days: 90
name: Build and Release MSI
on:
push:
branches:
- main # Release builds
- next # Prerelease builds
tags:
- 'v*' # Tagged releases
jobs:
build-msi:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Determine version
id: version
run: |
if ("${{ github.ref }}" -match "^refs/tags/v(.+)$") {
$VERSION = $matches[1]
$IS_RELEASE = "true"
} elseif ("${{ github.ref }}" -eq "refs/heads/next") {
$COMMIT = "${{ github.sha }}".Substring(0, 7)
$VERSION = "prerelease-$COMMIT"
$IS_RELEASE = "false"
} else {
$VERSION = "dev-${{ github.sha }}".Substring(0, 7)
$IS_RELEASE = "false"
}
echo "VERSION=$VERSION" >> $env:GITHUB_OUTPUT
echo "IS_RELEASE=$IS_RELEASE" >> $env:GITHUB_OUTPUT
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
cache: true
- name: Build Go binary
run: |
go build -ldflags "-X main.Version=${{ steps.version.outputs.VERSION }}" `
-o output/mcpproxy.exe ./cmd/mcpproxy
- name: Install WiX Toolset
run: dotnet tool install --global wix
- name: Build MSI
run: |
wix build installer/Product.wxs `
-o output/mcpproxy-${{ steps.version.outputs.VERSION }}-amd64.msi `
-d BuildVersion=${{ steps.version.outputs.VERSION }}
- name: Upload MSI artifact (Prerelease)
if: steps.version.outputs.IS_RELEASE == 'false'
uses: actions/upload-artifact@v4
with:
name: mcpproxy-installer-windows-amd64-prerelease-${{ github.sha }}
path: output/*.msi
compression-level: 0
retention-days: 30
- name: Upload MSI artifact (Release)
if: steps.version.outputs.IS_RELEASE == 'true'
uses: actions/upload-artifact@v4
with:
name: mcpproxy-installer-windows-amd64-${{ steps.version.outputs.VERSION }}
path: output/*.msi
compression-level: 0
retention-days: 90
- name: Create GitHub Release
if: steps.version.outputs.IS_RELEASE == 'true'
uses: softprops/action-gh-release@v1
with:
files: output/*.msi
draft: false
prerelease: false
name: Build MSI (WiX 3.x Legacy)
on:
push:
branches: [main]
jobs:
build-msi:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v1
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Build application
run: dotnet build --configuration Release
- name: Add WiX to PATH
run: |
echo "C:\Program Files (x86)\WiX Toolset v3.11\bin" | `
Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Compile WiX source
run: candle.exe installer/Product.wxs -out obj/Product.wixobj
- name: Link MSI
run: light.exe obj/Product.wixobj -out bin/Installer.msi
- name: Upload MSI artifact
uses: actions/upload-artifact@v4
with:
name: installer-msi
path: bin/Installer.msi
retention-days: 30
Basic Product.wxs Example:
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package
Name="MCPProxy"
Version="$(var.BuildVersion)"
Manufacturer="MCPProxy Contributors"
UpgradeCode="YOUR-GUID-HERE">
<MajorUpgrade
DowngradeErrorMessage="A newer version is already installed." />
<Media Id="1" Cabinet="mcpproxy.cab" EmbedCab="yes" />
<!-- Directory structure -->
<StandardDirectory Id="ProgramFiles64Folder">
<Directory Id="INSTALLFOLDER" Name="MCPProxy">
<Component Id="MainExecutable" Guid="YOUR-COMPONENT-GUID">
<File
Id="mcpproxy.exe"
Source="output/mcpproxy.exe"
KeyPath="yes" />
</Component>
<Component Id="TrayExecutable" Guid="YOUR-TRAY-COMPONENT-GUID">
<File
Id="mcpproxy_tray.exe"
Source="output/mcpproxy-tray.exe" />
</Component>
</Directory>
</StandardDirectory>
<!-- Feature definition -->
<Feature Id="Complete" Level="1">
<ComponentRef Id="MainExecutable" />
<ComponentRef Id="TrayExecutable" />
</Feature>
</Package>
</Wix>
<Component Id="MainExecutable" Guid="YOUR-COMPONENT-GUID">
<File
Id="mcpproxy.exe"
Source="output/mcpproxy.exe"
KeyPath="yes" />
<!-- Add to PATH -->
<Environment
Id="PATH"
Name="PATH"
Value="[INSTALLFOLDER]"
Permanent="no"
Part="last"
Action="set"
System="yes" />
</Component>
<Component Id="ServiceComponent" Guid="YOUR-SERVICE-GUID">
<File
Id="mcpproxy_service.exe"
Source="output/mcpproxy.exe"
KeyPath="yes" />
<ServiceInstall
Id="MCPProxyService"
Name="MCPProxyService"
DisplayName="MCPProxy Service"
Description="MCPProxy background service"
Type="ownProcess"
Start="auto"
ErrorControl="normal" />
<ServiceControl
Id="StartService"
Name="MCPProxyService"
Start="install"
Stop="both"
Remove="uninstall" />
</Component>
<StandardDirectory Id="DesktopFolder">
<Component Id="DesktopShortcut" Guid="YOUR-SHORTCUT-GUID">
<Shortcut
Id="DesktopShortcut"
Name="MCPProxy"
Target="[INSTALLFOLDER]mcpproxy-tray.exe"
WorkingDirectory="INSTALLFOLDER"
Icon="mcpproxy.ico" />
<RegistryValue
Root="HKCU"
Key="Software\MCPProxy"
Name="installed"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</StandardDirectory>
<!-- Icon definition -->
<Icon Id="mcpproxy.ico" SourceFile="assets/icon.ico" />
Product.wxs with Platform Variable:
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package
Name="MCPProxy ($(var.Platform))"
Version="$(var.BuildVersion)"
Manufacturer="MCPProxy Contributors"
UpgradeCode="YOUR-GUID-HERE">
<!-- Platform-specific properties -->
<?if $(var.Platform) = "x64" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?elseif $(var.Platform) = "arm64" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?endif ?>
<StandardDirectory Id="$(var.PlatformProgramFilesFolder)">
<Directory Id="INSTALLFOLDER" Name="MCPProxy">
<Component Id="MainExecutable" Guid="YOUR-COMPONENT-GUID">
<File
Id="mcpproxy.exe"
Source="$(var.BinaryPath)"
KeyPath="yes" />
</Component>
</Directory>
</StandardDirectory>
<Feature Id="Complete" Level="1">
<ComponentRef Id="MainExecutable" />
</Feature>
</Package>
</Wix>
Build Command:
wix build Product.wxs `
-o output/mcpproxy-$VERSION-amd64.msi `
-d BuildVersion=$VERSION `
-d Platform=x64 `
-d BinaryPath=output/mcpproxy-amd64.exe
Alternative Approach: Use GoReleaser with WiX support.
.goreleaser.yml:
builds:
- id: mcpproxy
main: ./cmd/mcpproxy
binary: mcpproxy
goos:
- windows
goarch:
- amd64
- arm64
msi:
- id: mcpproxy-msi
builds:
- mcpproxy
name: "MCPProxy-{{ .Version }}-{{ .Arch }}.msi"
wxs: installer/Product.wxs
extra_files:
- assets/icon.ico
GitHub Actions with GoReleaser:
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
actions/setup-go@v4compression-level: 0 for MSI uploads