For the purposes of automating the build of this site, I’ve been experimenting with git commands automation using PowerShell scripts. The overall goal is to automate in Visual Studio Services building this site using Hugo and then pushing the redenred files into a git branch to be rendered by github pages. I’ll describe more about this process in a following post. This post will focus on invoking git from PowerShell.
The most important part of this automation is being able to push a certain directory into a git repository. Most of the automation examples explain such concepts with bash scripts and there isn’t much for the Windows ecosystem. When landing into a git, PowerShell, Visual Studio Services and Windows related content, then the content is focused on how to consume a git repository and not how to execute the git to automate. Invoking git from a PowerShell script is not as easy as you would expect and it is even more complicated when the script is executed as part of a build step in Visual Studio Services. Since I started using Visual Studio Services, I’ve noticed a couple of differences with the PowerShell environment provided by the hosted agent.
Generally speaking, to execute a command from a PowerShell script, e.g. Test.exe
with parameters -P1 -P2 Value2
you have a couple of options
& Test.exe -P1 -P2 Value2
- Create an argument list variable
$arguments=@("-P1","-P2","Value2")
and then& Test.exe $arguments
- Use the same argument list variable
Start-Process Test.exe -ArgumentList $arguments -Wait
. Depending on whatTest.exe
does, you can control the parameters such as-Wait
-NoNewWindow
-PassThru
So I was expecting to do something similar for git but I run into a couple of problems:
- When using just
git
as the executable name, a new command prompt is launched regardless of the-NoNewWindow
parameter. - There were a lot of inconsistencies between the returned exit code, the output and error stream.
At the end and after a lot experimentation I landed into the following script block. This is not a fully optimized script and there can be many improvements. The important thing is that this script block executes well both from your local client and from a PowerShell build step in a Visual Studio Services build definition.
$invokeGit= {
Param (
[Parameter(
Mandatory=$true
)]
[string]$Reason,
[Parameter(
Mandatory=$true
)]
[string[]]$ArgumentsList
)
try
{
$gitPath=& "C:\Windows\System32\where.exe" git
$gitErrorPath=Join-Path $env:TEMP "stderr.txt"
$gitOutputPath=Join-Path $env:TEMP "stdout.txt"
if($gitPath.Count -gt 1)
{
$gitPath=$gitPath[0]
}
Write-Verbose "[Git][$Reason] Begin"
Write-Verbose "[Git][$Reason] gitPath=$gitPath"
Write-Host "git $arguments"
$process=Start-Process $gitPath -ArgumentList $ArgumentsList -NoNewWindow -PassThru -Wait -RedirectStandardError $gitErrorPath -RedirectStandardOutput $gitOutputPath
$outputText=(Get-Content $gitOutputPath)
$outputText | ForEach-Object {Write-Host $_}
Write-Verbose "[Git][$Reason] process.ExitCode=$($process.ExitCode)"
if($process.ExitCode -ne 0)
{
Write-Warning "[Git][$Reason] process.ExitCode=$($process.ExitCode)"
$errorText=$(Get-Content $gitErrorPath)
$errorText | ForEach-Object {Write-Host $_}
if($errorText -ne $null)
{
exit $process.ExitCode
}
}
return $outputText
}
catch
{
Write-Error "[Git][$Reason] Exception $_"
}
finally
{
Write-Verbose "[Git][$Reason] Done"
}
}
A couple of example of using the $invokeGit
are:
- Execute
git status -b --porcelain
$arguments=@( "status" "-b" "--porcelain" ) $status=Invoke-Command -ScriptBlock $invokeGit -ArgumentList "Status porcelain",$arguments
- Execute
git pull
$arguments=@( "pull" ) Invoke-Command -ScriptBlock $invokeGit -ArgumentList "Pull",$arguments
To help understand the script block, a couple of remarks:
- The block first finds the location of the git.exe. It seems that when providing the absolute path to
Start-Process
combined with-NoNewWindow
switch, the command prompt window does not launch. Therefore the script tries to locate first thegit.exe
path. - In your client client executing
where git
should return only one lineC:\Program Files\Git\cmd\git.exe
. If you try to do the same inside a Visual Studio Services build agent, then you get two identical lines. So for compatibility reasons, the script checks if the outcome is an array. Use the full path ofC:\Windows\System32\where.exe
to avoid launching a new command prompt window. - As I mentioned there are problems with the output and error stream. At the end I’m forwarding the output and error stream to two files and the process their content. Sometimes the return exit code is not zero but the error stream is empty.
The control reasoning is as follows:
- Capture the error and output streams into different files.
- If the output’s file content is not null, then pipe each line to
Write-Host
. - if the returned exit code is not zero then
- Write a warning
- If the error’s file content is not null, then pipe each line to
Write-Host
. Exit the entire script with git’s exit code. In my case, I can’t allow the script to continue if git fails, therefore I can simply exit the script. Even better, the non zero exit code is captured by Visual Studio Services agent and marks the step as failed.
- As a personal flavor of tracing in PowerShell, I tend to wrap block actions in patterns such as
[Reason]
. The$invokeGit
adds a prefix as[Git][$Reason]
where the$Reason
is provided as a parameter from the main script.
Leave a Comment