May 18, 2026

Automating .NET Framework Desktop App Releases on GitHub

Setting up Continuous Integration and Continuous Deployment (CI/CD) for modern .NET Core applications is usually straightforward. However, if you are maintaining a legacy .NET Framework desktop application (like WPF or WinForms using .NET 4.5 or 4.8), compiling and releasing it via GitHub Actions requires a specific set of tools: Windows runners, MSBuild, and a bit of PowerShell configuration.

Based on my experience, we will walk through the entire process of setting up a push-to-deploy pipeline that automatically builds your .NET Framework solution, zips the output, and publishes a formal GitHub Release.

The Release Process: From Code to Deployment

Here is how the automated lifecycle works once configured:

  1. Write Code & Push: You make changes to your local application and push the commits to your GitHub repository.
  2. Add the Workflow: You create a YAML configuration file inside a .github/workflows/ directory in your repository. This file acts as the blueprint, telling GitHub's servers exactly how to build your app.
  3. Trigger the Action: You tell GitHub to run the workflow either by pushing a Git Tag (e.g., v1.0) from your terminal, or by manually clicking a "Run workflow" button in the GitHub Actions tab.
  4. Automated Build & Package: GitHub spins up a temporary Windows server. It restores your NuGet packages, compiles your solution using MSBuild, and runs a background script to compress your executable and DLL files into a ZIP archive.
  5. Publish Release: The finished ZIP file is automatically uploaded and attached to a new GitHub Release page, making it instantly available for your users to download.

How to Structure Your GitHub Action

While we won't look at the raw YAML script, understanding the logical steps your action must take is critical for .NET Framework apps.

  • Choose the Right Runner: You must configure the action to run on windows-latest so it has access to Visual Studio build tools.
  • Restore Dependencies: Use the NuGet setup action to restore your packages. It is highly recommended to explicitly tell NuGet to drop the packages into a root packages folder so MSBuild can easily locate them.
  • Targeting Packs: If you are targeting an older framework like .NET 4.5, modern GitHub runners will not have it pre-installed. You will need to use a PowerShell step to download the targeting pack directly from NuGet and place it in the Windows Reference Assemblies folder.
  • Build with MSBuild: Use the MSBuild setup action to compile your solution file. Make sure to specify the Release configuration.
  • Zip and Release: Use a simple PowerShell command (like Compress-Archive) to zip your output folder, and pass that ZIP file to a GitHub Release action.

    
  name: Build and Release FaceSearchVLC 2

  on:
    push:
      tags:
        - 'v*'
    workflow_dispatch:

  jobs:
    build-and-release:
      runs-on: windows-latest

      steps:
        - name: Checkout source code
          uses: actions/checkout@v4

        - name: Setup NuGet
          uses: nuget/setup-nuget@v2

        - name: Restore NuGet packages
          run: nuget restore nfwFolderCheckGithub.sln

        - name: Install .NET 4.5 Targeting Pack
          shell: pwsh
          run: |
            $url = "https://www.nuget.org/api/v2/package/Microsoft.NETFramework.ReferenceAssemblies.net45/1.0.3"
            Invoke-WebRequest -Uri $url -OutFile "net45.zip"
            Expand-Archive -Path "net45.zip" -DestinationPath "net45"
            $target = "C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5"
            New-Item -ItemType Directory -Path $target -Force
            Copy-Item -Path "net45\build\.NETFramework\v4.5\*" -Destination $target -Recurse -Force

        - name: Pre-Build Fixes (HintPaths & Windows.winmd)
          shell: pwsh
          run: |
            # 1. Fix NuGet Paths: Copy root packages into the project folders so MSBuild can find them
            if (Test-Path "packages") {
                Write-Host "Copying packages to project directories..."
                Copy-Item -Path "packages" -Destination "ConsoleToast\packages" -Recurse -Force
                Copy-Item -Path "packages" -Destination "nfwFolderCheck\packages" -Recurse -Force
            }

            # 2. Fix WinRT APIs: Find Windows.winmd on the runner and place it in the output folder
            $winmd = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\UnionMetadata" -Filter "Windows.winmd" -Recurse | Select-Object -First 1
            if ($winmd) {
                Write-Host "Found Windows.winmd. Copying to build directory..."
                New-Item -ItemType Directory -Force -Path "ConsoleToast\bin\Release"
                Copy-Item $winmd.FullName -Destination "ConsoleToast\bin\Release\Windows.winmd"
            }

        - name: Setup MSBuild
          uses: microsoft/setup-msbuild@v2

        - name: Build the Solution
          run: msbuild nfwFolderCheckGithub.sln /p:Configuration=Release /p:Platform="Any CPU" /p:SignManifests=false

        - name: Prepare and Zip Release Assets
          shell: pwsh
          run: |
            New-Item -ItemType Directory -Force -Path _ReleaseOutput

            if (Test-Path "nfwFolderCheck\bin\Release\*") {
                Copy-Item -Path "nfwFolderCheck\bin\Release\*" -Destination _ReleaseOutput -Recurse -Force
            }

            if (Test-Path "ConsoleToast\bin\Release\*") {
                Copy-Item -Path "ConsoleToast\bin\Release\*" -Destination _ReleaseOutput -Recurse -Force
            }

            Compress-Archive -Path _ReleaseOutput\* -DestinationPath FaceSearchVLC-Release.zip

        - name: Publish GitHub Release
          uses: softprops/action-gh-release@v2
          with:
            files: FaceSearchVLC-Release.zip
            generate_release_notes: true
            # This line checks if a tag triggered the run. 
            # If not (manual run), it creates a tag like "v1.0.4" based on the run number.
            tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v1.0.{0}', github.run_number) }}
          env:
            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    
  

Troubleshooting Common .NET Framework Build Errors

When compiling older desktop apps on cloud runners, you will likely run into a few specific MSBuild errors. Here is exactly how to fix them.

Error: MSB1009: Project file does not exist

The Problem: MSBuild throws an error referencing a placeholder name like your-solution-name. This happens if you copy-pasted a template and left placeholder variables intact.

The Fix: Hardcode your actual solution file name (e.g., MyApplication.sln) directly into the MSBuild run command inside your workflow.

Error: MSB3644: Reference assemblies for .NETFramework,Version=v4.5 were not found

The Problem: GitHub updated their modern Windows runners to Visual Studio 2022, which dropped built-in support for .NET 4.5. Do not try to downgrade to older runners (like windows-2019), as they have been permanently retired and will cause your build to freeze.

The Fix: Keep the runner on the latest version and add a PowerShell step right before MSBuild to manually download the 4.5 targeting pack from NuGet and copy it into your runner's Reference Assemblies directory.

Error: MSB3323: Unable to find manifest signing certificate

The Problem: If your project uses ClickOnce deployment, Visual Studio generated a local certificate (a .pfx file) on your personal computer. GitHub's runner does not have this file, so the build crashes.

The Fix: Append /p:SignManifests=false to your MSBuild command to tell the compiler to ignore the missing local certificate during automated cloud builds.

Error: Missing Namespaces (CS0234 or CS0246)

The Problem: You get compilation errors saying external types (like ToastNotification or WindowsAPICodePack) could not be found. This happens because GitHub restores NuGet packages to the repository root, but older project files often look for them in specific local subfolders or hardcoded C:\ drive paths.

The Fix: Add a "Pre-Build" PowerShell step to dynamically copy the restored packages folder into the exact directory your project expects. If you rely on native Windows 10 APIs, use PowerShell to search the runner's UnionMetadata folder for Windows.winmd and copy it directly into your build folder before compiling.

Error: GitHub Releases requires a tag

The Problem: The final release step fails with a warning when you manually click "Run workflow". This happens because a formal GitHub Release cannot be created without a Git tag attached to it.

The Fix: You can either trigger the workflow strictly by pushing Git tags from your terminal, or update your release action configuration to automatically generate a fallback tag (like formatting the run number into a version string) whenever a tag isn't detected.

0 comments: