22

I use Git to deploy my web application. So in Teamcity I prepare my application (compilation, minify JS and HTML, delete unused files, etc...) and then I have a Powershell build step :

$ErrorActionPreference = "Stop"
git init
git remote add origin '%env.gitFolder%'
git fetch
git reset --mixed origin/master
git add .
git commit -m '%build.number%'
git push origin master

But if an exception throw, the script continue (even if I set $ErrorActionPreference = "Stop") and the build is successful.

I want the script to stop when there is an error, and the build to failed.

I tried to put Format stderr output as: error on the build step and Fail build if: an error message is logged by build runner, so the build failed, but the script continue, and so it's creating an stupid commit.

I try to put a try-catch in my script, but it doesn't enter in the catch...

Does anyone have an idea to stop the script and failed the build on error ?

Sorry for my English, I'm French... ^^

1
  • 1
    TeamCity's handling of Powershell errors isn't very intuitive. I've previously given this piece of advice - but you're saying that Try/Catch aren't helping. Commented Feb 19, 2018 at 11:31

2 Answers 2

32

The $ErrorActionPreference variable does not apply to calling external utilities ([console] applications) such as git, invariably in Windows PowerShell and PowerShell (Core) 7 up to 7.3.x, and by default in 7.4.+

  • In PowerShell 7.4+, opt-in integration with PowerShell's error handling is available, via the $PSNativeCommandUseErrorActionPreference preference variable.

    • If you set this variable to $true, any external-program call that reports a nonzero exit code automatically triggers a PowerShell error in response, which then integrates with PowerShell's own error handling.
      That is, with the $ErrorActionPreference preference variable set to 'Stop', any external program reporting a nonzero exit code will then result in a script-terminating (fatal by default) error.

There are only two ways to determine success vs. failure of an external utility:

  • By examining the automatic $LASTEXITCODE variable, which PowerShell sets to the exit code reported by the external utility.
    By convention, a value of 0 indicates success, whereas any nonzero value indicates failure. (Do note that some utilities, e.g., robocopy.exe, use certain nonzero exit codes to communicate non-error conditions too.)

  • If you're not interested in the specific exit code reported, you can examine the Boolean automatic variable $?, which reflects $True for exit code 0, and $False for any nonzero exit code.

    • [Fixed in PowerShell 7.2+] Caveats: As of PowerShell (Core) 7.1, there are two bugs; both these are tentatively fixed via the PSNotApplyErrorActionToStderr experimental feature (!) in preview versions of 7.2 (all experimental features are turned on by default in newly installed preview versions).

      • $? can falsely reflect $false when $LASTEXITCODE is 0 and a stderr redirection (2> or *>) is used and the command emitted stderr output (which in itself doesn't necessarily indicate failure) - see GitHub issue #10512.

        • It is therefore a good habit to form to query only $LASTEXITCODE, not $? to infer failure vs. succcess.
      • If $ErrorActionPreference is set to 'Stop' and you happen to use 2> or *> with an external program and that program produces actual stderr output, $ErrorActionPreference unexpectedly does apply and causes a script-terminating error (whether or not the external program signals actual failure via a nonzero exit code) - see GitHub issue #4002

  • If you want to capture stderr output lines, you have to redirect them to a file as of PowerShell 7.2.x (e.g., 2>errs.txt; see caveat above re PowerShell 7.1 and below); GitHub issue #4332 suggests introducing a syntax that allows in-memory capturing, in a variable. However, there is an indirect way of capturing them in memory, namely by merging stderr into the success output stream with 2>&1, which allows you to filter lines that came from stderr by the .NET type of the objects in the success output stream - see this answer.

Acting on a failure requires explicit action, typically by using the Throw keyword to generate a script-terminating error.


Clearly, checking $LASTEXITCODE / $? after every external-utility call is cumbersome, so here's a wrapper function that facilitates this process:

Note: While this function works, it does not try to compensate for the broken-up-to-at-least-v7.2.x passing of arguments with embedded double quotes to external programs. To get this compensation, you can use the iee function from the Native module, installable from the PowerShell Gallery via Install-Module Native.

function Invoke-Utility {
<#
.SYNOPSIS
Invokes an external utility, ensuring successful execution.

.DESCRIPTION
Invokes an external utility (program) and, if the utility indicates failure by 
way of a nonzero exit code, throws a script-terminating error.

* Pass the command the way you would execute the command directly.
* Do NOT use & as the first argument if the executable name is not a literal.

.EXAMPLE
Invoke-Utility git push

Executes `git push` and throws a script-terminating error if the exit code
is nonzero.
#>
  $exe, $argsForExe = $Args
  # Workaround: Prevents 2> redirections applied to calls to this function
  #             from accidentally triggering a terminating error.
  #             See bug report at https://github.com/PowerShell/PowerShell/issues/4002
  $ErrorActionPreference = 'Continue'
  try { & $exe $argsForExe } catch { Throw } # catch is triggered ONLY if $exe can't be found, never for errors reported by $exe itself
  if ($LASTEXITCODE) { Throw "$exe indicated failure (exit code $LASTEXITCODE; full command: $Args)." }
}

Now you just need to prepend Invoke-Utility  to all your git calls, and if any of them reports a nonzero exit code, the script is aborted.

If that is too verbose, define an alias for your function: Set-Alias iu Invoke-Utility, in which case you only need to prepend iu :

iu git init
iu git remote add origin '%env.gitFolder%'
iu git fetch
# ...
Sign up to request clarification or add additional context in comments.

5 Comments

For some native programs in powershell 7.0.3 $LASTEXITCODE reports non zero at the same time as $? reports True. i.imgur.com/4P9au0c.png on this screenshot, packer is packer.
And just now I realised what's happening - both packer and go is installed with scoop which creates a shim around the executable. The shim is .ps1 which affects the results. Case closed.
@EvilNeo, please see my update, but the short of it is: As of PS 7.1 there is no direct way of capturing them in a variable; you need to save to a file first. A future enhancement may remedy this. An indirect way is to merge stderr into the success output stream with 2>&1 and then identify the stderr lines in the output streams by their .NET type.
If we use iee do we still need to check $LASTEXITCODE or something else to make the script fail?
@CMCDragonkai, no, iee automatically uses throw to raise a script-terminating (fatal) error if $LASTEXITCODE is nonzero. Try iee cmd /c exit 1; "this won't print", for instance.
14

I believe the problem here is that the error thrown by git is not trappable by PS.

Illustration:

try {
    git push
    Write-Host "I run after the git push command"
}
catch {
    Write-Host "Something went wonky"
}

Note the missing <code>Write-Host</code> from the <code>catch</code> block!

Note the missing Write-Host from the catch block!

This is where we need to look at the exit code of the git commands.

The easiest way (I know) in PowerShell is to check the value of $? (more information on $? here: What is `$?` in Powershell?)

try {
    git push
    if (-not $?) {
        throw "Error with git push!"
    }
    Write-Host "I run after the git push command"
}
catch {
    Write-Host "Something went wonky"
    throw
}

Check our custom error (now caught by the <code>catch</code> block)!

Check our custom error (now caught by the catch block)!

3 Comments

Thanks a lot for this explanation ! But, like I have many lines in my powershell script, I can't do that on every lines, it will be unreadable...
If readability is your concern; create a wrapper function that does the "error handling" for you. Functionality is more important than readability. But yes; maintainability should always maintain a prominent position!
You can one-line your check if that helps: if ($?) {throw}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.