Research Date: 2025-11-13 Target: Building WiX Toolset 4.x installers for Go applications on GitHub Actions
- Pre-installed Tools on Windows Runners
- WiX Toolset Installation Methods
- Artifact Management Best Practices
- Windows-Specific Performance Considerations
- Real-World Workflow Examples
- Go Application + WiX Integration
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:
- 8.0.121, 8.0.206, 8.0.318, 8.0.415
- 9.0.111, 9.0.205, 9.0.306
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:
- Az: 12.5.0
- AWSPowershell: 5.0.88
- PowerShellGet: 1.0.0.1, 2.2.5
- PSScriptAnalyzer: 1.24.0
Visual Studio Enterprise 2022:
- Version: 17.14.36623.8
- Path:
C:\Program Files\Microsoft Visual Studio\2022\Enterprise
MSBuild:
- Component: Microsoft.Component.MSBuild
- Version: 17.14.36510.44
- Included with Visual Studio Enterprise 2022
Windows SDK Versions:
- 10.0.19041
- 10.0.22621
- 10.0.26100
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
- Repository: actions/runner-images
- Current README: Windows2022-Readme.md
- Software tools are updated weekly
Installation:
- name: Install WiX Toolset
run: dotnet tool install --global wixRequirements:
- .NET SDK 6.0 or later (pre-installed on
windows-latest) - WiX package on nuget.org (automatically resolved)
Building:
- name: Build MSI
run: wix build -o output/installer.msi Product.wxsWith 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:
- Simple installation (single command)
- Consistent with modern .NET tooling
- Works with WiX 4.x
- Fastest installation method (< 10 seconds)
Cons:
- Requires NuGet.org connectivity
- Less familiar to WiX 3.x users
Installation:
- name: Install WiX via Chocolatey
run: choco install wixtoolset -yUsage:
- 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.msiPros:
- Familiar to WiX 3.x users
- Traditional candle/light workflow
Cons:
- WiX 3.x is legacy (not recommended for new projects)
- Longer installation time (30-60 seconds)
- Caching Chocolatey installs is difficult on GitHub Actions
- WiX 3.x is no longer actively maintained
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=ReleasePros:
- Integrates with existing MSBuild projects
- Supports complex build configurations
- Good for .NET-heavy projects
Cons:
- More setup overhead
- Requires .wixproj file
| 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:
- GitHub Free: 500 MB storage
- GitHub Pro: 2 GB storage
- Private Repos: 10 GB/month transfer limit
- Public Repos: Higher allowances
Individual Artifact Limits:
- Maximum per-artifact size: Not explicitly documented, but practical limit is ~10 GB
- GitHub Releases: 2 GB per file (separate from artifact storage)
Job Limits:
- Maximum 500 artifacts per job
Default Retention:
- Public repositories: 90 days (adjustable: 1-90 days)
- Private repositories: 90 days (adjustable: 1-400 days)
Custom Retention:
- name: Upload MSI artifact
uses: actions/upload-artifact@v4
with:
name: installer-msi
path: output/*.msi
retention-days: 30 # Override defaultImportant Notes:
- Retention period only applies to NEW artifacts (not retroactive)
- Artifacts auto-delete after the retention period
- Cannot exceed repository/organization/enterprise limits
Default Compression:
- Default level: 6 (same as GNU Gzip)
- Algorithm: Zlib
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 rangeCompression Guidelines:
- Level 0: No compression (fastest upload, largest size) - Recommended for pre-compressed files (ZIP, MSI)
- Level 6: Default (balanced speed/size)
- Level 9: Maximum compression (slowest upload, smallest size)
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:
- Include architecture:
windows-amd64,windows-arm64 - Include version:
${{ github.ref_name }}or${{ github.sha }} - Distinguish release types:
prerelease,release,nightly - Use hyphens for readability
Important: 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:
- .NET SDK installation (if not cached): 20-30 seconds
- WiX tool installation: < 10 seconds
- Go build (1 binary): 5-15 seconds
- WiX compile/link: 10-30 seconds
- Total workflow: 2-5 minutes
Drive Performance Comparison:
- C: drive: Standard performance
- D: drive: 20-30% faster for large builds
- ReFS-based VHDX: Up to 30X faster IOPS
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 buildRequirements:
- Enable lock files in project:
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> - For multi-project repos:
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.0Note: 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: drivePros: 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: 30name: 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: 90name: 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: falsename: 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: 30Basic 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.exeAlternative 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.icoGitHub Actions with GoReleaser:
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}- Use WiX Toolset 4.x with .NET global tool installation
- Cache Go dependencies with
actions/setup-go@v4 - Use compression-level: 0 for MSI artifacts (already compressed)
- Separate prerelease and release artifacts with descriptive names
- Multi-architecture support with build matrix (amd64, arm64)
- Retention: 30 days for prerelease, 90 days for release
- Enable Go dependency caching
- Enable .NET NuGet caching (if applicable)
- Cache .NET global tools with fixed version
- Use
compression-level: 0for MSI uploads - Consider D: drive for large builds (optional)
- Simple build (single arch): 2-3 minutes
- Multi-arch build (amd64 + arm64): 4-6 minutes
- With full caching: 1-2 minutes