为什么我的PowerShell退出代码总是“0”?

78

我有一个如下的PowerShell脚本

##teamcity[progressMessage 'Beginning build']
# If the build computer is not running the appropriate version of .NET, then the build will not run. Throw an error immediately.
if( (ls "$env:windir\Microsoft.NET\Framework\v4.0*") -eq $null ) {
    throw "This project requires .NET 4.0 to compile. Unfortunately .NET 4.0 doesn't appear to be installed on this machine."
    ##teamcity[buildStatus status='FAILURE' ]
}

##teamcity[progressMessage 'Setting up variables']
# Set up variables for the build script
$invocation = (Get-Variable MyInvocation).Value
$directorypath = Split-Path $invocation.MyCommand.Path
$v4_net_version = (ls "$env:windir\Microsoft.NET\Framework\v4.0*").Name
$nl = [Environment]::NewLine

Copy-Item -LiteralPath "$directorypath\packages\NUnit.2.6.2\lib\nunit.framework.dll" "$directorypath\Pandell.Tests\bin\debug" -Force

##teamcity[progressMessage 'Using msbuild.exe to build the project']
# Build the project using msbuild.exe.
# Note we've already determined that .NET is already installed on this computer.
cmd /c C:\Windows\Microsoft.NET\Framework\$v4_net_version\msbuild.exe "$directorypath\Pandell.sln" /p:Configuration=Release
cmd /c C:\Windows\Microsoft.NET\Framework\$v4_net_version\msbuild.exe "$directorypath\Pandell.sln" /p:Configuration=Debug

# Break if the build throws an error.
if(! $?) {
    throw "Fatal error, project build failed"
    ##teamcity[buildStatus status='FAILURE' ]
}

##teamcity[progressMessage 'Build Passed']
# Good, the build passed
Write-Host "$nl project build passed."  -ForegroundColor Green


##teamcity[progressMessage 'running tests']
# Run the tests.
cmd /c $directorypath\build_tools\nunit\nunit-console.exe $directorypath\Pandell.Tests\bin\debug\Pandell.Tests.dll

# Break if the tests throw an error.
if(! $?) {
    throw "Test run failed."
    ##teamcity[buildStatus status='FAILURE' ]
}

##teamcity[progressMessage 'Tests passed']

据我了解,未捕获的Throw会导致退出代码为1,但不幸的是TeamCity表示不同意。
[19:32:20]Test run failed.
[19:32:20]At C:\BuildAgent\work\e903de7564e599c8\build.ps1:44 char:2
[19:32:20]+     throw "Test run failed."
[19:32:20]+     ~~~~~~~~~~~~~~~~~~~~~~~~
[19:32:20]    + CategoryInfo          : OperationStopped: (Test run failed.:String) [],
[19:32:20]   RuntimeException
[19:32:20]    + FullyQualifiedErrorId : Test run failed.
[19:32:20]
[19:32:20]Process exited with code 0
[19:32:20]Publishing internal artifacts
[19:32:20][Publishing internal artifacts] Sending build.finish.properties.gz file
[19:32:20]Build finished

重要提示:我的“执行模式”设置为“使用“-File”参数执行.ps1脚本”。我试着将其更改为“使用“-Command -”参数将脚本放入PowerShell标准输入”,但是即使测试通过,它仍然会以退出代码“1”失败。我确定以“-File”的方式运行将是正确的方法。如果我在CMD中手动打开位于“C:\BuildAgent\work\e903de7564e599c8\build.ps1”的脚本并运行它,它也会做同样的事情……即,失败的测试会失败,并且“%errorlevel%”仍然为“0”。然而,如果我在PowerShell中运行它并调用“$LASTEXITCODE”,它每次都会返回正确的代码。

4
throw 之后的代码不会被执行。 - Aleš Roubíček
1
如果您将构建步骤的错误级别从“警告”更改为“错误”,会有什么区别吗? - devlord
我遇到了同样的问题。只需将您的throw替换为简单的“exit -1”。 - Derek Greer
相关链接:https://dev59.com/cGgu5IYBdhLWcg3wAigi - Ruben Bartelink
1
可能是如何在TeamCity PowerShell运行程序中传播错误的重复问题。 - Michael Freidgeim
显示剩余2条评论
5个回答

98

这是PowerShell已知的问题,使用-file参数运行脚本时返回0的退出代码,但实际上不应该返回0。

(更新:下面的链接已经无法使用,请在PowerShell: Hot (1454 ideas) – Windows Server上寻找或报告此问题)

如果使用-command也无效,您可以尝试在脚本顶部添加一个trap语句:

trap
{
    write-output $_
    ##teamcity[buildStatus status='FAILURE' ]
    exit 1
}

当抛出异常时,以上代码应该产生正确的退出码。


2
你能解释一下为什么这个陷阱会起作用吗?我明天回到项目时会测试它。 - Chase Florell
6
建议使用 write-output $_ 可行,而 Write-Error -ErrorRecord $_ 会生成类似于 PowerShell 本身的精美错误输出。 - Mike
有没有使用'$_'和字符串换行的理由,而不是将Write-Output "##teamcity..."全部放在一行上? - Robin
这个关键部分是从“trap”中以status=1的状态退出。在退出之前,您可以按照自己的喜好构建输出和/或执行其他操作。 - Kevin Richardson
2
如果按照 @Mike 的建议使用 Write-Error,请确保您的 $ErrorActionPreference 没有设置为 "Stop",否则脚本将以代码 0 退出,而 exit 1 将永远无法到达。 - EM0
显示剩余3条评论

25

我在使用-file运行时遇到了完全相同的问题,但出于某种原因,Kevin提供的陷阱语法或'exit'语法在我的场景中无效。

我不确定为什么,但以防其他人遇到同样的问题,我使用了下面的语法,它对我起作用:

try{
    #DO SOMETHING HERE
}
catch
{
    Write-Error $_
    ##teamcity[buildStatus status='FAILURE']
    [System.Environment]::Exit(1)
}

我没有尝试过陷阱,但是用简单的“exit -1”重放抛出操作对我来说解决了这个问题。 - Derek Greer

17

直到这个问题(可能)被关闭为重复 我之前的自问自答, 我将在这里总结最干净的解决方案:

  • 大多数其他答案都涉及从PowerShell部分发出一些内容到stderr。可以通过TeamCity直接完成此操作,通过格式化标准错误输出选项(将其设置为Error而不是默认值,即Warning

  • 然而,关键是还需要在“失败条件”下开启“如果:...生成运行器记录了错误消息”(如果其他答案中有任何一个适用于您,您可能已经开启了此功能,但根据我的经验,很容易忘记!)


3
这绝对是本页面上最干净的解决方案。 - lantrix
1
这将导致您的构建失败,但是即使设置为仅在成功运行时运行,所有后续的构建步骤仍将执行。 - Craig Brett
@craigBrett 如果确认是个大问题,那显然是个大问题 - 我目前没有使用 TC(叹气!)所以如果这条评论和这条评论可以得出什么结论,对其他读者来说将是一个很大的帮助。 - Ruben Bartelink
这是从我正在解决的当前问题开始的。这就是为什么我分享我的经验的原因。但我同意这个问题可能困扰了很多人。 - Craig Brett
请注意,这似乎会将一些良性的消息解释为错误,例如 npm 警告和 webpack 输出(如果正在构建 JS 应用程序)。 - Zach Esposito

2

在命令中使用-ErrorAction stop会默认返回退出码1,并且在不添加失败条件的情况下也会在TeamCity中显示。现在我们将为每个PowerShell命令默认实现此行为,方法是使用$ErrorActionPreference = "Stop";


我尝试了这个网站和其他网站上的很多方法,但都没有成功。也许是因为我的powershell一行代码: (Get-Content -path package.json -Raw) -replace '"version": "0.0.0"','"version": "0.0.1"' | Set-Content -Path package.json 没有抛出异常? 无论如何,我改用了仅以上述-ErrorAction选项,并且现在该步骤将失败,例如如果文件丢失。我将其添加到了Get-Content和Set-Content中。 - ColH

1

由于某些原因,这些选项都无法在我的PowerShell脚本中正常工作。我花费了数小时的时间。

对我来说,最好的选择是在TeamCity和PowerShell之间加入一层。因此,我只需编写一个调用PowerShell脚本的C#控制台应用程序。

我的做法是,在TeamCity中调用一个名为:RemoteFile.ps1的脚本。

带有脚本参数:%system.RemoteServerFQDN% %system.RemoteUser% %system.RemoteUserPassword% %system.RemoteScriptName% %system.RemotePropertiesFile% %system.BuildVersion% %system.RunList%

param (
    [Parameter(Mandatory=$true)]
    $Computername,
    [Parameter(Mandatory=$true)]
    $Username,
    [Parameter(Mandatory=$true)]
    $Password,
    [Parameter(Mandatory=$true)]
    $ScriptName,
    [Parameter(Mandatory=$true)]
    $Propfile,
    [Parameter(Mandatory=$true)]
    $Version,
    [Parameter(Mandatory=$true)]
    [string[]]$DeploymentTypes
)

$securePassword = ConvertTo-SecureString -AsPlainText -Force $Password
$cred = New-Object System.Management.Automation.PSCredential $Username, $securePassword
Write-Host "Readying to execute invoke-command..."
Invoke-Command -ComputerName $Computername -Credential $cred -ScriptBlock {       D:\Deployment\PowershellWrapper.exe $using:ScriptName $using:Propfile $using:Version      $using:DeploymentTypes } -ArgumentList $ScriptName,$Propfile,$Version,$DeploymentTypes

该文件存在于指定位置的远程服务器上。

然后该文件调用位于指定位置的powershellwrapper.exe(我的脚本有四个参数需要传递给PowerShell脚本)。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;

namespace PowershellWrapper
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                string argFull = @"""{0} {1} {2} {3}""";
                string arg0 = args[0];
                string arg1 = args[1];
                string arg2 = args[2];
                string arg3 = args[3];
                string argFinal = string.Format(argFull, arg0, arg1, arg2, arg3);

                ProcessStartInfo startInfo = new ProcessStartInfo();
                startInfo.FileName = @"powershell.exe";
                startInfo.Arguments = argFinal;
                startInfo.RedirectStandardOutput = false;
                startInfo.RedirectStandardError = false;
                startInfo.UseShellExecute = false;
                startInfo.RedirectStandardInput = true;
                startInfo.CreateNoWindow = false;
                Process process = new Process();
                process.StartInfo = startInfo;
                process.Start();
            }
            catch (Exception e)
            {
                Console.WriteLine("{0} Exception caught.", e);
                Console.WriteLine("An error occurred in the deployment.", e);
                Console.WriteLine("Please contact test@test.com if error occurs.");
            }
        }
    }
}

这会使用四个参数调用我的脚本。第一个参数是脚本,后面还有三个参数。所以基本上这里发生的事情是我执行 PowershellWrapper.exe 而不是 PowerShell 脚本本身,以捕获错误退出代码 0,并将完整的脚本报告回 TeamCity 日志。
我希望这样说起来有意义。对我们来说它非常好用。

似乎不需要在脚本中添加瑕疵或其他辅助工具,我的答案似乎可以等效地工作 - 只是在TeamCity中文档不是很好。请参阅https://dev59.com/cGgu5IYBdhLWcg3wAigi。 - Ruben Bartelink

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接