Building Windows Executables with GitHub Actions
Introduction
GitHub Actions enables developers to build, test, and package software entirely in the cloud. One particularly powerful capability is the ability to produce native Windows executables (.exe) without using Windows locally.
This article provides a detailed, end-to-end explanation of how that process works. It covers what GitHub Actions actually runs, how Windows binaries are produced, what happens during compilation, and why the resulting executable is indistinguishable from one built on a local Windows machine.
The goal is not just to show how it works, but to explain why it works.
What GitHub Actions Is at a Technical Level
GitHub Actions is a hosted automation and continuous integration platform. When a workflow is triggered, GitHub provisions a runner, which is a temporary virtual machine.
Important characteristics of a runner:
- Created on demand
- Fully isolated
- Preconfigured with common build tools
- Destroyed immediately after the workflow completes
GitHub provides runners for multiple operating systems:
- Linux
- Windows
- macOS
When a workflow specifies a Windows runner, GitHub spins up a real Windows virtual machine, not an emulator or compatibility layer.
Why Your Local Operating System Does Not Matter
When using GitHub Actions, no compilation happens on your local machine.
Your system is only used to:
- Edit source code
- Push commits to GitHub
- Download build artifacts
All build steps execute inside GitHub’s infrastructure.
Conceptually, the flow looks like this:
Local machine > GitHub repository > GitHub-hosted Windows runner > Windows executable (.exe) > Downloadable artifact
Because the compilation occurs on a Windows system hosted by GitHub, the output is a genuine Windows binary regardless of whether the developer uses Linux, macOS, or another platform locally.
Anatomy of a Typical GitHub Actions Workflow
A simplified workflow for building a Windows executable from a .NET project looks like this:
name: Build
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 6.0.x
- name: Build
run: dotnet publish -c Release -r win-x64 --self-contained true
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-output
path: bin/Release/net6.0/win-x64/publish/
Each line corresponds to a concrete operation performed on the Windows runner.
Selecting the Windows Build Environment
runs-on: windows-latest
This directive instructs GitHub to allocate a Windows virtual machine.
That machine includes:
- A Windows kernel and filesystem
- Native Windows APIs
- Support for Portable Executable (PE) output
- Preinstalled system libraries and build utilities
Because the build runs on Windows itself, the generated executable fully conforms to Windows expectations.
Retrieving the Source Code
- uses: actions/checkout@v4
This step clones the repository into the runner’s filesystem.
At this point, the runner has:
- All source files
- Project configuration files
- Dependency definitions
- Build scripts
The environment mirrors what a developer would see after cloning the repository locally.
Installing the Compiler and Toolchain
- uses: actions/setup-dotnet@v4
This step installs the .NET SDK, which includes:
- The C# compiler (Roslyn)
- The .NET CLI (dotnet)
- Build and publish tooling
This is the same compiler stack used by local development environments such as Visual Studio.
What Actually Happens During the Build
dotnet publish -c Release -r win-x64 --self-contained true
This command performs multiple stages internally.
Compilation to Intermediate Language
- C# source code is compiled into Intermediate Language (IL)
- Type information, method signatures, and metadata are generated
- The result is platform-agnostic managed code
Packaging into a Windows Executable
- IL and metadata are embedded into a Portable Executable (PE) container
- The file conforms to the Windows .exe format
- Entry points and headers are generated according to Windows standards
Runtime Bundling
Because --self-contained true is specified:
- The .NET runtime is bundled with the executable
- The output does not depend on a preinstalled framework
- The executable is portable across compatible Windows systems
The final output is a standalone Windows binary.
Artifact Storage and Retrieval
- uses: actions/upload-artifact@v4
Artifacts are files preserved after the workflow finishes.
Key properties:
- Stored by GitHub after the runner is destroyed
- Downloadable via the GitHub web interface or API
- Immutable once uploaded
Artifacts are the only persistent output of the workflow, as the runner itself is ephemeral.
This Is Not Cross-Compilation
This process is often misunderstood as cross-compilation, but it is not.
Cross-compilation would involve:
- A non-Windows system producing Windows binaries directly
In contrast, GitHub Actions:
- Runs the build on a native Windows system
- Uses Windows toolchains
- Produces binaries exactly as a local Windows machine would
The developer is delegating compilation to a remote Windows environment, not translating binaries across platforms.
Determinism and Clean Build Environments
Because each runner is:
- Freshly provisioned
- Free of local configuration drift
- Destroyed after use
Builds are:
- Highly reproducible
- Consistent across runs
- Isolated from developer-specific environments
This eliminates many common sources of build inconsistency.
Mental Model to Remember
GitHub Actions can be understood as:
A temporary, disposable Windows machine that builds your software and disappears, leaving only the results behind.
Conclusion
GitHub Actions enables reliable production of Windows executables without requiring local Windows infrastructure. By leveraging hosted Windows runners and official toolchains, it provides:
- Native Windows builds
- Reproducible results
- Clean, isolated environments
- Minimal local setup
Understanding this model clarifies why the generated executables behave exactly like those built on a local Windows system and why the developer’s local platform is irrelevant to the final output.