PSake Turtles All The Way Down

I have been trying for a while to come up with a way to redesign a long PowerShell script into discrete PSake Tasks. Lots of the code is repeated throughout the script as small components are required in different configurations for different scenarios.

My original approach was to move chunks of repeated code to separate .psake.ps1 files and name tasks according to the component and the action. For example, Component1-CopyLocal and Component1-RunInstaller, while other components had similar long-winded names to avoid namespace clashes when each file was included the larger scenario/parent script.



Task Component1-CopyFiles -Description 'Copy component 1 files from network to local folder' {

    Write-Host "Copying component1.msi from network to local folder"
}


Task Component1-Install -Depends Component1-CopyFiles -Description 'Installs component 1 to local machine' {

    Write-Host "Installing component1.msi"
}



Task Component2-CopyFiles -Description 'Copy component 2 files from network to local folder' {

    Write-Host "Copying component2.msi from network to local folder"
}


Task Component2-Install -Depends Component2-CopyFiles -Description 'Installs component 2 to local machine' {

    Write-Host "Installing component2.msi"
}


# Install scenario: Install dependent components before running main script.

# Include other tasks in this script
Include '.\Component1-PSake.ps1'
Include '.\Component2-PSake.ps1'


Task ? -Description 'List tasks' -alias 'Help' { WriteDocumentation }
Task Default -Depends RunTool 


Task RunTool -Depends DeploymentDependencies -Description 'Runs the newly installed version of the tool' {

    Write-Host 'Running tool after dependencies installed'
}

Task DeploymentDependencies -Depends Component1-Install, Component2-Install, ...

This arrangement had three particular disadvantages, task names were particularly long but not really descriptive or helpful, the calling script had to know the names of each of the child tasks (or at least the names of all of the initiating tasks) making refactoring and renaming difficult and, finally, none of the child components were able to be standalone due to the namespace clash for the Default task.

That all changed when I discovered PSake has support for nested builds. We can redesign each component to be standalone, reduce the names of each task so they make sense at the component level, hide all of the task names from the calling scripts while still preserving the reuse of scripts and the mix-and-match nature that I wanted to begin with.


# Install Component 1

Task Default -Depends CopyFiles, RunInstaller


Task CopyFiles -Description 'Copy component 1 files from network to local folder' {

    Write-Host "Copying component1.msi from network to local folder"
}


Task RunInstaller -Depends CopyFiles -Description 'Installs component 1 to local machine' {

    Write-Host "Installing component1.msi"
}


# Install Component 2

Task Default -Depends CopyFiles, RunInstaller


Task CopyFiles -Description 'Copy component 2 files from network to local folder' {

    Write-Host "Copying component2.msi from network to local folder"
}


Task RunInstaller -Depends CopyFiles -Description 'Installs component 2 to local machine' {

    Write-Host "Installing component2.msi"
}


# Install scenario: Install dependent components before running main script.

Task ? -Description 'List tasks' -alias 'Help' { WriteDocumentation }
Task Default -Depends RunTool 


Task RunTool -Depends InstallComponent1, InstallComponent2 -Description 'Runs the newly installed version of the tool' {

    Write-Host 'Running tool after dependencies installed'
}


Task InstallComponent1 {

    Invoke-Psake .\Component1-PSake.ps1 -NoLogo
}


Task InstallComponent2 {

    Invoke-Psake .\Component2-PSake.ps1 -NoLogo 
}

And of course, we can repeat this nesting as needed with each component.

Error Handling

One thing to watch out for is error handling and exceptions when calling nested psake scripts. If a sub-task throws an exception, psake won't report that problem in the calling script and will just terminate all succeeding tasks. You will need to wrap the Invoke-Psake call in a Try block and use Write-Error in the Catch. To continue with succeeding tasks in a psake script, use the ContinueOnError switch in a Task that may throw.