Update, October 2026 — three of these tips have aged, the rest have not.
The core idea still stands: the post-build event box is a terrible editor, so get out of it on the first line and do the real work in a script file that lives in the project and is under source control. What has changed:
- Tip 0 is obsolete. Visual Studio has been a 64-bit process since VS 2022, so the “use PowerShell (x86)” advice no longer applies. And please do not
Set-ExecutionPolicy unrestrictedmachine-wide any more — pass-ExecutionPolicy Bypasson the single invocation instead. Same result, no permanent hole.- The
FtpWebRequestbonus tip is obsolete. The whole type is marked obsolete in .NET 6 and later (SYSLIB0014). UseHttpClientagainst an HTTPS endpoint, or whatever your artefact store offers.- Tips 1 to 6 still work as written, including the 7-Zip password trick and the assembly-version reading. If you are starting fresh today, consider doing this in an MSBuild
TargetwithAfterTargets="Build"instead of a post-build event — you get conditions and item groups for free. But if the script already exists, there is no reason to rewrite it.The text from here on is the 2014 original, unchanged.
Visual Studio is doing a great job when it comes to building your software. But if you need to do additional steps after building (e.g. steps needed to release the software), there is really only minimal support: you get a minimal window where you can input a batch file. And if you open the macro pane you even cannot move the splitter… WTF?! So, our first task is to hand over to PowerShell — I use PowerShell ISE here for developing the script.
We at code4ward need to be able to release new software versions even faster. This not only goes for the existing Royal TS for Windows product but as well another new product that we are going to release soon. For this, we need to automate the steps we have to do for building and releasing our software as much as possible. Stefan Koell and I spent a nice afternoon putting together our automation script — and some useful tips came out as well ;)
Tip 0: Configure PowerShell to execute scripts (not a tip, really)
Be aware that Visual Studio 2013 is still a 32-bit process, so you need to use the 32-bit version of PowerShell: on the Start screen type “Windows PowerShell (x86)”, execute it as Administrator(!) and issue the following command:
Set-ExecutionPolicy unrestrictedIf you run this command as non-admin, you get a nasty exception.
Tip 1: Immediately get out of that post-build event and use PowerShell
We recommend to start PowerShell and do the rest of your logic/code there.
But since Visual Studio is executing this synchronously, you don’t see any output in the output window until the script has ended — so you can start it asynchronously:
powershell "start-process powershell.exe -ArgumentList '-NoExit', '$(ProjectDir)Resources\tools\PostBuild.ps1', '$(ProjectDir)', '$(TargetDir)'"Let’s take this line apart:
- the first
powershellstarts the script — which immediately starts another script (usingstart-process). - the
-ArgumentListis used to pass arguments to the script.-NoExittells PowerShell to remain open after the execution finished (so we can check the output). On a build server you don’t want this option. PostBuild.ps1is the actual script.- Since it is located inside
$(ProjectDir)it is also source controlled.
Tip 2: Use Visual Studio 2013 macros in your PowerShell
It’s handy that Visual Studio is giving us variables out of the box. We just need to hand them over to the PowerShell script — the last two parameters in the line above are variables from Visual Studio that are passed to the script. PostBuild.ps1 starts with:
param (
[string]$ProjectDir,
[string]$TargetDir
)Now you have the macro variables from Visual Studio 2013 available in your script — no need to hardcode anything ;)
Tip 3: Executing the PostBuild.ps1 only if needed
Now, you probably don’t need to execute the script with every build. Using the Visual Studio Configuration Manager you can handle this in a flexible way. We introduced a “Publish” build target, and in the post-build event script editor you can access this information:
if $(ConfigurationName) == Publish (
powershell "start-process powershell.exe -ArgumentList '-NoExit', '$(ProjectDir)Resources\tools\PostBuild.ps1', '$(ProjectDir)', '$(TargetDir)'"
)The variable $(ConfigurationName) is your friend here.
Tip 4: Zipping your build with a password
It is easy to zip your build output, but if you want to secure it with a password the standard .NET classes leave you alone. You can use 7-Zip from the command line (download the Portable Apps version) and zipping is very easy:
$ZipTool = Join-Path -path $ProjectDir -childpath "Resources\Tools\7z.exe"
function create-zip([String] $zipTool, [String] $aDirectory, [String] $aZipfile){
[Array]$arguments = "a", "-tzip", "$aZipfile", "$aDirectory", "-pSECUREPWD", "-r"
& $zipTool $arguments
}
$Name = "zipped_build.zip"
$zfn = [System.Environment]::ExpandEnvironmentVariables("%temp%\$Name")
create-zip $ZipTool (Join-Path -Path $OutPath -ChildPath "*.*") $zfnPlease notice the $zfn variable: we zip the output in the temp directory (where we have write access) using an environment variable, which needs to be expanded before. The 7z.exe is located inside $(ProjectDir) — so every developer has the tool on their machine (and since it’s the Portable App version, no installation is needed).
Also take notice of the zip password — you might pass this into the script and not hardcode this one here.
Tip 5: Working with build version information
You might need to work with version information from the build DLLs: major, minor, build and revision. This is also easily achieved with PowerShell using the reflection namespace from .NET:
$file = "yourApp.exe"
$ass = [System.Reflection.Assembly]::LoadFile($file)
$v = $ass.GetName().Version
$version = [string]::Format("{0}.{1:00}.{2:00}.{3}", $v.Major, $v.Minor, $v.Build, $v.Revision)$v gives you access to all these version numbers — in this case it is used to create a special version that has a leading 0 for the minor and build number if needed.
Tip 6: Replacing content in a file
For replacing some variables in a template, PowerShell again is your friend:
function replace-file-content([String] $path, [String] $replace, [String] $replaceWith)
{
(Get-Content $path) |
Foreach-Object {$_ -replace $replace, $replaceWith} |
Out-File $path
}This function replaces text and is called like this:
replace-file-content $ChangeLogOutPath "{Major}" $v.Major.ToString()Bonus Tip 7: Uploading the build via FTP including a PowerShell progress bar
$ftpWebRequest = [System.Net.FtpWebRequest]::Create((New-Object System.Uri("ftp://your_ftp_server")))
$ftpWebRequest.Method = [System.Net.WebRequestMethods+Ftp]::UploadFile
$inputStream = [System.IO.File]::OpenRead($filePath)
$outputStream = $ftpWebRequest.GetRequestStream()
[byte[]]$buffer = New-Object byte[] 131072
$totalReadBytesCount = 0
while (($readBytesCount = $inputStream.Read($buffer, 0, $buffer.Length)) -gt 0)
{
$outputStream.Write($buffer, 0, $readBytesCount)
$totalReadBytesCount += $readBytesCount
$progress = [int]($totalReadBytesCount * 100.0 / $inputStream.Length)
Write-Progress -Activity "Uploading file..." -PercentComplete $progress -CurrentOperation "$progress% complete" -Status "Please wait."
}
$outputStream.Close()
$outputStream = $null
Write-Progress -Activity "Uploading file..." -Completed -Status "Done!"First, we create an FtpWebRequest object and instruct it to upload a file. We loop through $inputStream.Read(...), write to the output stream and update the $progress variable which is used in the PowerShell Write-Progress cmdlet to update the “UI”. Handy if your upload takes some time. You might want to adapt the size of the byte buffer according to your file size and upload speed.
