Skip to main content

Exit Codes

Understanding exit codes is crucial for integrating psake with CI/CD systems and handling build failures correctly.

Overview

psake returns exit codes to indicate the success or failure of a build:

  • Exit code 0: Build succeeded
  • Exit code 1: Build failed

The exit code is set automatically based on whether all tasks completed successfully or if any errors occurred during execution.

Exit Code Behavior

Success (Exit Code 0)

psake returns exit code 0 when:

  • All specified tasks complete without errors
  • All task dependencies run successfully
  • No exec commands fail (unless using -continueOnError)
  • No assertions fail
  • No unhandled exceptions are thrown
  • All postconditions pass
# Successful build
Invoke-psake -taskList Build

# Check exit code
echo $LASTEXITCODE # Output: 0

Failure (Exit Code 1)

psake returns exit code 1 when:

  • Any task throws an unhandled exception
  • Any exec command fails (returns non-zero exit code)
  • Any assertion fails
  • Any postcondition fails
  • Required variables are missing
  • Circular task dependencies are detected
  • Build script has syntax errors
# Failed build
Invoke-psake -taskList Deploy

# Check exit code
echo $LASTEXITCODE # Output: 1

Checking Exit Codes

In PowerShell

# Method 1: Check $LASTEXITCODE
Invoke-psake -buildFile .\psakefile.ps1 -taskList Build

if ($LASTEXITCODE -ne 0) {
Write-Host "Build failed with exit code: $LASTEXITCODE" -ForegroundColor Red
exit $LASTEXITCODE
} else {
Write-Host "Build succeeded" -ForegroundColor Green
}
# Method 2: Use try-catch
try {
Invoke-psake -buildFile .\psakefile.ps1 -taskList Build
if ($LASTEXITCODE -ne 0) {
throw "Build failed"
}
}
catch {
Write-Error "Build error: $_"
exit 1
}
# Method 3: Use ErrorAction
Invoke-psake -buildFile .\psakefile.ps1 -taskList Build -ErrorAction Stop

# Execution stops here if build fails
Write-Host "Build succeeded"

In Bash/Shell Scripts

#!/bin/bash

# Run psake build
pwsh -Command "Invoke-psake -buildFile ./psakefile.ps1 -taskList Build"

# Check exit code
if [ $? -eq 0 ]; then
echo "Build succeeded"
else
echo "Build failed with exit code: $?"
exit 1
fi
# Using set -e (exit on error)
#!/bin/bash
set -e # Exit immediately if any command fails

pwsh -Command "Invoke-psake -taskList Build"
echo "Build succeeded"

In Batch Files (Windows)

@echo off

REM Run psake build
pwsh -Command "Invoke-psake -taskList Build"

REM Check exit code
if %ERRORLEVEL% neq 0 (
echo Build failed with exit code: %ERRORLEVEL%
exit /b %ERRORLEVEL%
)

echo Build succeeded

CI/CD Integration

GitHub Actions

name: Build

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install psake
shell: pwsh
run: Install-Module -Name psake -Scope CurrentUser -Force

- name: Run build
shell: pwsh
run: |
Invoke-psake -taskList Build
if ($LASTEXITCODE -ne 0) {
throw "Build failed"
}

# Alternative: Let GitHub Actions detect failure automatically
- name: Run build (simpler)
shell: pwsh
run: |
Invoke-psake -taskList Build
exit $LASTEXITCODE

Note: GitHub Actions automatically fails the job if any step returns a non-zero exit code.

Azure Pipelines

trigger:
- main

pool:
vmImage: 'ubuntu-latest'

steps:
- pwsh: |
Install-Module -Name psake -Scope CurrentUser -Force
displayName: 'Install psake'

- pwsh: |
Invoke-psake -taskList Build
if ($LASTEXITCODE -ne 0) {
Write-Error "Build failed"
exit 1
}
displayName: 'Run build'

# Alternative: Use failOnStderr
- pwsh: |
Invoke-psake -taskList Build
exit $LASTEXITCODE
displayName: 'Run build'
failOnStderr: false

GitLab CI

build:
image: mcr.microsoft.com/powershell:latest
script:
- pwsh -Command "Install-Module -Name psake -Force"
- pwsh -Command "Invoke-psake -taskList Build; exit $LASTEXITCODE"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"

Note: GitLab CI fails the job if the script returns non-zero.

Jenkins

pipeline {
agent any

stages {
stage('Build') {
steps {
pwsh '''
Install-Module -Name psake -Scope CurrentUser -Force
Invoke-psake -taskList Build

if ($LASTEXITCODE -ne 0) {
throw "Build failed with exit code: $LASTEXITCODE"
}
'''
}
}
}

post {
failure {
echo 'Build failed!'
}
success {
echo 'Build succeeded!'
}
}
}

TeamCity

<!-- Build Step: PowerShell -->
<buildStep type="powershell">
<param name="script.content">
Install-Module -Name psake -Scope CurrentUser -Force
Invoke-psake -taskList Build

if ($LASTEXITCODE -ne 0) {
Write-Error "Build failed"
exit 1
}
</param>
<param name="teamcity.step.mode">default</param>
</buildStep>

Common Exit Code Scenarios

Scenario 1: External Command Fails

Task Build {
# dotnet build fails (returns exit code 1)
exec { dotnet build }
}

# Result: psake returns exit code 1
# Reason: exec detected non-zero exit code from dotnet

CI/CD Impact: Build job fails automatically.

Scenario 2: Assertion Fails

Properties {
$Version = ''
}

Task Deploy {
Assert (![string]::IsNullOrEmpty($Version)) "Version is required"
exec { ./deploy.ps1 -Version $Version }
}

# Result: psake returns exit code 1
# Reason: Assertion failed (Version is empty)

CI/CD Impact: Build job fails with clear error message.

Scenario 3: Postcondition Fails

Task Build -postcondition { Test-Path './build/app.dll' } {
exec { dotnet build -o ./build }
}

# If app.dll is not created:
# Result: psake returns exit code 1
# Reason: Postcondition failed

CI/CD Impact: Build job fails after task completes.

Scenario 4: Task with ContinueOnError

Task Test -continueOnError {
exec { dotnet test }
# Even if tests fail, build continues
}

Task Package -depends Test {
exec { dotnet pack }
}

# Result: psake returns exit code 0 (if Package succeeds)
# Reason: Test failures were ignored due to -continueOnError

Warning: Use -continueOnError carefully in CI/CD. Failed tests may not fail the build.

Scenario 5: Multiple Tasks, One Fails

Invoke-psake -taskList Clean, Build, Test

# If Build fails:
# - Clean runs successfully
# - Build fails (exit code 1)
# - Test doesn't run (dependency failed)
# Result: psake returns exit code 1

CI/CD Impact: Build job stops at first failure.

Exit Code Best Practices

1. Always Check Exit Codes in CI/CD

# BAD: Doesn't check exit code
Invoke-psake -taskList Build
echo "Build complete"

# GOOD: Checks exit code
Invoke-psake -taskList Build
if ($LASTEXITCODE -ne 0) {
throw "Build failed"
}
echo "Build succeeded"

2. Use Exec for External Commands

# BAD: Doesn't detect failures
Task Build {
dotnet build
# Build continues even if dotnet fails
}

# GOOD: Automatically detects failures
Task Build {
exec { dotnet build }
# Build fails if dotnet fails
}

3. Fail Fast with Assertions

# Validate prerequisites before expensive operations
Task Deploy {
Assert (![string]::IsNullOrEmpty($ApiKey)) "API_KEY is required"
Assert (Test-Path $ArtifactPath) "Build artifacts not found"
Assert ($Environment -in @('Staging', 'Production')) "Invalid environment"

# Expensive deployment only runs if assertions pass
exec { ./deploy.ps1 }
}

4. Provide Meaningful Error Messages

# BAD: Generic error
Task Build {
exec { dotnet build }
}

# GOOD: Clear error message
Task Build {
exec { dotnet build } -errorMessage "Failed to compile solution. Check for syntax errors."
}

5. Don't Swallow Errors

# BAD: Hides failures
Task Test {
try {
exec { dotnet test }
}
catch {
Write-Warning "Tests failed, but continuing..."
# Build succeeds even though tests failed!
}
}

# GOOD: Proper error handling
Task Test {
try {
exec { dotnet test }
}
catch {
Write-Error "Tests failed: $_"
throw # Re-throw to fail the build
}
}

6. Use Exit Codes in Wrapper Scripts

# build.ps1 - Wrapper script for psake
param(
[string]$Task = 'Default',
[hashtable]$Parameters = @{}
)

# Install psake if needed
if (-not (Get-Module psake -ListAvailable)) {
Install-Module psake -Scope CurrentUser -Force
}

Import-Module psake

# Run build
Invoke-psake -buildFile ./psakefile.ps1 -taskList $Task -parameters $Parameters

# Propagate exit code
exit $LASTEXITCODE

7. Handle Exit Codes in Multi-Stage Builds

# build.ps1 - Multi-stage build script
$tasks = @('Clean', 'Restore', 'Build', 'Test', 'Package')

foreach ($task in $tasks) {
Write-Host "Running task: $task" -ForegroundColor Cyan

Invoke-psake -taskList $task

if ($LASTEXITCODE -ne 0) {
Write-Error "Task '$task' failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
}
}

Write-Host "All tasks completed successfully" -ForegroundColor Green

Troubleshooting Exit Codes

Problem: Build Succeeds Locally but Fails in CI

Possible causes:

  1. Environment variables missing in CI
  2. Different PowerShell version in CI
  3. Missing dependencies in CI environment
  4. Case-sensitive file paths (Linux CI)

Solution:

Task ValidateEnvironment {
Assert ($env:API_KEY) "API_KEY environment variable is required"
Assert ($PSVersionTable.PSVersion.Major -ge 7) "PowerShell 7+ required"
Assert (Get-Command dotnet -ErrorAction SilentlyContinue) "dotnet CLI not found"
}

Task Build -depends ValidateEnvironment {
exec { dotnet build }
}

Problem: Exit Code is 0 but Build Should Fail

Possible causes:

  1. Not using exec for external commands
  2. Using -continueOnError inappropriately
  3. Catching exceptions without re-throwing

Solution:

# Always use exec
Task Build {
exec { dotnet build } # Fails if dotnet fails
}

# Don't catch without re-throwing
Task Test {
try {
exec { dotnet test }
}
catch {
Write-Error "Tests failed: $_"
throw # Re-throw to fail build
}
}

Problem: Can't Determine Why Build Failed

Solution: Enable verbose output and check error details:

# Verbose output
Invoke-psake -Verbose

# Capture detailed errors
$ErrorActionPreference = 'Stop'
try {
Invoke-psake -taskList Build
}
catch {
Write-Host "Build failed with error:" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor Gray
exit 1
}

Exit Codes in Docker

FROM mcr.microsoft.com/powershell:latest

WORKDIR /build

# Install psake
RUN pwsh -Command "Install-Module -Name psake -Scope CurrentUser -Force"

# Copy build files
COPY . .

# Run build and propagate exit code
CMD ["pwsh", "-Command", "Invoke-psake -taskList Build; exit $LASTEXITCODE"]

Verify in Docker:

# Build Docker image
docker build -t myapp-build .

# Run build
docker run --rm myapp-build

# Check exit code
echo $? # 0 for success, 1 for failure

See Also