This post summarizes how one can build a ClickOnce package automatically in GitHub Actions with publishing to GitHub Pages.
Assuming you have a WPF application in a GitHub repository (see list of real examples using this method below).
First, create publish profile for ClickOnce package locally in Visual Studio (see Microsoft docs). Right-click on your project, select Publish and ClickOnce. Leave default options, but set “Install location” as “From a web site”, and specify the URL as:
https://{user}.github.io/{repo}/
replacing
{user}
with your GitHub username and{repo}
with the repository name.This should create file
Properties/PublishProfiles/ClickOnceProfile.pubxml
. Check it into your repository and publish to yourmain
branch, it will be needed by the build script below.You don’t need to click “Publish”, pushing the publish profile (
.pubxml
) file is enough.Create an empty branch
gh-pages
and push it to GitHub:git switch --orphan gh-pages git commit --allow-empty -m "Initial commit" git push -u origin gh-pages
Enable GitHub Pages on your repository (in Settings > Pages) and configure them to publish from
gh-pages
branch (set folder to root/
).In your
gh-pages
branch, add.gitattributes
with the following content:* -text
to prevent Git from messing with line endings in your release files.
Also add an empty
.nojekyll
file to prevent GitHub from processing your release files before publishing them.In your
main
branch, createrelease.ps1
script as below (don’t forget to change the two properties at the beginning marked with “👈”). Optionally, you can embed this code directly into the GitHub Action in the next step, but then you won’t be able to run the release locally which might be useful (you can run./release.ps1 -OnlyBuild
from PowerShell on your machine to perform only the build step without publishing to GitHub pages).# From https://janjones.me/posts/clickonce-installer-build-publish-github/. [CmdletBinding(PositionalBinding=$false)] param ( [switch]$OnlyBuild=$false ) $appName = "WpfApplication" # 👈 Replace with your application project name. $projDir = "WpfApplication" # 👈 Replace with your project directory (where .csproj resides). Set-StrictMode -version 2.0 $ErrorActionPreference = "Stop" Write-Output "Working directory: $pwd" # Find MSBuild. $msBuildPath = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" ` -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe ` -prerelease | select-object -first 1 Write-Output "MSBuild: $((Get-Command $msBuildPath).Path)" # Load current Git tag. $tag = $(git describe --tags) Write-Output "Tag: $tag" # Parse tag into a three-number version. $version = $tag.Split('-')[0].TrimStart('v') $version = "$version.0" Write-Output "Version: $version" # Clean output directory. $publishDir = "bin/publish" $outDir = "$projDir/$publishDir" if (Test-Path $outDir) { Remove-Item -Path $outDir -Recurse } # Publish the application. Push-Location $projDir try { Write-Output "Restoring:" dotnet restore -r win-x64 Write-Output "Publishing:" $msBuildVerbosityArg = "/v:m" if ($env:CI) { $msBuildVerbosityArg = "" } & $msBuildPath /target:publish /p:PublishProfile=ClickOnceProfile ` /p:ApplicationVersion=$version /p:Configuration=Release ` /p:PublishDir=$publishDir /p:PublishUrl=$publishDir ` $msBuildVerbosityArg # Measure publish size. $publishSize = (Get-ChildItem -Path "$publishDir/Application Files" -Recurse | Measure-Object -Property Length -Sum).Sum / 1Mb Write-Output ("Published size: {0:N2} MB" -f $publishSize) } finally { Pop-Location } if ($OnlyBuild) { Write-Output "Build finished." exit } # Clone `gh-pages` branch. $ghPagesDir = "gh-pages" if (-Not (Test-Path $ghPagesDir)) { git clone $(git config --get remote.origin.url) -b gh-pages ` --depth 1 --single-branch $ghPagesDir } Push-Location $ghPagesDir try { # Remove previous application files. Write-Output "Removing previous files..." if (Test-Path "Application Files") { Remove-Item -Path "Application Files" -Recurse } if (Test-Path "$appName.application") { Remove-Item -Path "$appName.application" } # Copy new application files. Write-Output "Copying new files..." Copy-Item -Path "../$outDir/Application Files","../$outDir/$appName.application" ` -Destination . -Recurse # Stage and commit. Write-Output "Staging..." git add -A Write-Output "Committing..." git commit -m "Update to v$version" # Push. git push } finally { Pop-Location }
Again in your
main
branch, create a GitHub Action workflow file.github/workflows/release.yml
:name: Release on: push: tags: [v*] jobs: release: runs-on: windows-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Setup Git run: | git config --global url."https://user:${{ secrets.GITHUB_TOKEN }}@github".insteadOf https://github git config --global user.name github-actions git config --global user.email github-actions@github.com - name: Run release script shell: pwsh run: ./release.ps1
This will run whenever you publish a release using the GitHub UI or manually push a tag (which is what publishing the release does under the hood):
git tag v1.0.0 git push origin v1.0.0
Note that the script expects the tag to be specified with prefix
v
.When the workflow runs, it builds your app, creates a ClickOnce package, and pushes it to the
gh-pages
branch, so it’s available for download at the URL you specified in step 1 as file{app}.application
where{app}
is the name of your application project:https://{user}.github.io/{repo}/{app}.application
Beware that the URL is case sensitive (so if the app name is
WpfApplication
, the URL could behttps://jjonescz.github.io/wpf-example/WpfApplication.application
).Now you can provide that URL to your users and let the ClickOnce magic happen. Whenever you publish a new version of the app, it will be automatically downloaded to your users when they open the app (and have an Internet connection).
Real-world examples
These are real-world examples of .NET applications published using the above method:
- InOculus: Minimal desktop timer to remind you to get up and relieve eyestrain