I've made a useful PowerShell script to automate our code deployments to Salesforce. After a couple of afternoons learning PS and some try/error, I finished my script and it's working.

Nonetheless, since it's my first long PS script I was hoping you guys could tip me in how I could do things better, make the script more readable, etc. I'm used to OOP, but this is a whole different story from what I've seen lol

Just as a quick introduction, what the code does is:

  1. Validates that the repository I want to deploy to a Salesforce org has a package.xml (a file which enumerates what I'm going to deploy, like a check list)

  2. Authenticates against Salesforce

  3. Deploys all the repository and polls the server to update the status in the console

  4. Runs the test and keeps polling to update the visual status

  5. Reacts to deployment/test errors by printing them on the console

In order to achieve this I used the sfdx CLI which Salesforce provides, so it did save me a LOT of time.

param([string] $repositoryDirectory, [bool] $isProduction)

Write-Host -ForegroundColor green ":: Validating Repository ::"
$srcDirectory = [System.IO.Path]::Combine($repositoryDirectory, "src")
if(![System.IO.File]::Exists([System.IO.Path]::Combine($srcDirectory,"package.xml"))) {
    Write-Host -ForegroundColor red "ERROR: package.xml not found in the ./src directory."

Write-Host -ForegroundColor green "`n:: Authenticating to Salesforce ::"
if($isProduction) {
    sfdx force:auth:web:login -s -r "https://login.salesforce.com"
else {
    sfdx force:auth:web:login -s -r "https://test.salesforce.com"

Write-Host -ForegroundColor green "`n:: Deploying source code ::"
$deployJob = sfdx force:mdapi:deploy -d $srcDirectory -l "RunLocalTests" --json | ConvertFrom-Json
$deployJobId = $deployJob.result.id

$startedTests = $false
do {
    $report = sfdx force:mdapi:deploy:report -i $deployJobId --json --verbose 2>&1 | ConvertFrom-Json 

    if($null -eq $report) {

    # Deployment progress block.
    if($report.result.numberComponentsTotal -ne 0 -and $componentsRemaining -ne 0) {
        $deploymentRatio = [Math]::Ceiling(100 * ($report.result.numberComponentsDeployed / $report.result.numberComponentsTotal))
        $componentsRemaining = $report.result.numberComponentsTotal - $report.result.numberComponentsDeployed - $report.result.numberComponentsFailed

        # If the percentage is not yet 100%, update it.
        if($deploymentRatio -le 100) {
            Write-Host -NoNewLine "`rComponents deployed: " $deploymentRatio "%"

        # If the deployment has failed
        if($report.result.status -eq "Failed") {

    # Write next header.
    if(($report.result.numberTestsTotal -ne 0) -and ($startedTests -eq $false)) {
        $startedTests = $true
        Write-Host -ForegroundColor green "`n`n:: Running tests ::"

    # Write Test progress
    if($report.result.numberTestsTotal -ne 0 -and $testsRemaining -ne 0) {
        $testRatio = [Math]::Ceiling((100 * ($report.result.numberTestsCompleted / $report.result.numberTestsTotal)))
        $testsRemaining = $report.result.numberTestsTotal - $report.result.numberTestErrors - $report.result.numberTestsCompleted

        Write-Host -NoNewLine "`rTests passed: " $testRatio "% | Tests remaining: " $testsRemaining

    if($testsRemaining -eq 0 -and $report.result.numberTestErrors -gt 0) {
        Write-Host -ForegroundColor red "`nERROR: $($report.result.numberTestErrors) tests have failed"

} while(($report.result.status -eq "InProgress") -or ($report.result.status -eq "Pending"))

 if($report.result.status -eq "Failed") {
    Write-Host -ForegroundColor red "`n`nERROR Deployment Failed!"
    $report = sfdx force:mdapi:deploy:report -i $deployJobId --json --verbose 2>&1 | ConvertFrom-Json

    foreach($failure in $report.result.details.componentFailures) {
        Write-Host -ForegroundColor red "`t - " $failure.problem


Write-Host -ForegroundColor green "`n:: Deployment Successful! ::"

1 Answer 1


In parameters for your script, you usually want a [switch] instead of [bool]. It's more convenient to use when launching the script and works the same way in the script.

Since your script doesn't work without a repositoryDirectory, you should make this a mandatory parameter. PowerShell provides a lot of convenient error handling out of the box if you annotate your parameters properly.

    [parameter(Mandatory=$true)] [string] $RepositoryDirectory,
    [switch] $IsProduction

PS> .\Deploy-SF.ps1 'Repository' -IsProduction

Write-Host is generally frowned upon. It keeps the text out of the standard stream and is strictly for display in the console. You can't capture this text in a log file for example, by redirecting the script output. Here you use it for colored text, which is a reason. But if you can get by without the green text, you can simply put your string as a statement on a line. It will get end up in the output stream automatically. And PowerShell expands variables automatically inside of double quotes.

# instead of
Write-Host "Tests passed: " $testRatio "% | Tests remaining: " $testsRemaining
# use
"Tests passed: $testRatio% | Tests remaining: $testsRemaining"
# wrap complex expressions in $()
"Time = $($Some.Object.Duration()) Hours"

If you want to do fancy tricks with overwriting the same line repeatedly, you can do as you did or read and write $Host.UI.RawUI.CursorPosition.

You can display a progress bar and some additional info using Write-Progress.

Don't use red text for errors. Use Write-Warning, Write-Error or throw accordingly (depending on if you want a terminating error or not).

Use PowerShell commandlets instead of .NET classes whenever possible. It's easier to read, parameters are easily discoverable etc.

# instead of
# use
Test-Path $path
Join-Path $srcDirectory "package.xml"
  • \$\begingroup\$ Hi Swonkie! Thanks much for the Code Review. I noted everything down. How would you use the $Host.UI.RawUI.CursorPosition? \$\endgroup\$ Aug 7, 2018 at 10:25
  • 1
    \$\begingroup\$ You can store the value of CursorPosition in a variable. Then you generate some output (can be multiple lines), then you set CursorPosition back to the original value and generate the updated output, which will overwrite the old text. \$\endgroup\$
    – user176538
    Aug 7, 2018 at 11:10

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.