When writing GitHub Actions workflows, it is generally recommended to keep your scripts in separate files and reference them using the path attribute of the run step. This allows you to lint and test your scripts easily using standard tools.
However, this approach is not always practical. For instance, when creating a reusable workflow or a simple action, you might prefer to keep everything in a single file. In such cases, you often end up writing PowerShell scripts directly in the run step of your workflow.
The downside of inline scripts is that you cannot easily check their syntax before pushing them to GitHub. You have to wait for the workflow to run to discover that you missed a closing bracket or used an invalid operator.
In this post, I'll show you a PowerShell function that parses a GitHub Actions workflow file, extracts all PowerShell scripts from run steps, and checks their syntax without executing them.
#Prerequisites
To parse the YAML files, we'll use the powershell-yaml module. You can install it using the following command:
PowerShell
Install-Module -Name powershell-yaml -Scope CurrentUser -Force
#The Script
The script consists of two functions:
Test-PowerShellScript: This function takes a string containing PowerShell code and uses the System.Management.Automation.Language.Parser class to check for syntax errors. It also replaces GitHub expressions (e.g. ${{ github.job }}) with a placeholder variable to avoid false positives. It does not execute the code.Test-GhaPwshSteps: This function parses the GitHub Actions workflow file, iterates through all jobs and steps, identifies the steps that use PowerShell, and validates the script content using Test-PowerShellScript.
Here is the complete script:
PowerShell
# Function to test PowerShell script syntax
# It does not execute the script, only parses it to find syntax errors
function Test-PowerShellScript {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)]
[string]$ScriptText
)
# Replace GitHub expressions with a placeholder variable to avoid syntax errors
# e.g. ${{ github.job }} -> $GitHubExpression
$ScriptText = $ScriptText -replace '\$\{\{.*?\}\}', '$GitHubExpression'
$tokens = $null
$errors = $null
try {
[void][System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [ref]$tokens, [ref]$errors)
} catch {
return @{ IsValid = $false; Errors = @($_) }
}
if ($errors -and $errors.Count -gt 0) {
return @{ IsValid = $false; Errors = $errors }
}
return @{ IsValid = $true; Errors = @() }
}
# Find all PowerShell steps in a GitHub Actions workflow and test their syntax.
# The function returns a list of syntax errors found in PowerShell steps.
function Test-GhaPwshSteps {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$Path
)
if (-not (Get-Module -ListAvailable -Name powershell-yaml)) {
Write-Warning "The 'powershell-yaml' module is required. Please run: Install-Module -Name powershell-yaml -Scope CurrentUser"
return
}
Import-Module powershell-yaml
if (-not (Test-Path $Path)) {
Write-Error "File not found: $Path"
return
}
$yamlContent = Get-Content -Path $Path -Raw
try {
$workflow = ConvertFrom-Yaml $yamlContent
} catch {
Write-Error "Failed to parse YAML: $_"
return
}
# 1. Determine Global Default Shell
$globalShell = $null
if ($workflow.defaults -and $workflow.defaults.run -and $workflow.defaults.run.shell) {
$globalShell = $workflow.defaults.run.shell
}
$results = @()
# 2. Iterate Jobs
if ($workflow.jobs) {
foreach ($jobKey in $workflow.jobs.PSObject.Properties.Name) {
$job = $workflow.jobs.$jobKey
# Determine Job Default Shell (overrides global)
$jobShell = $globalShell
if ($job.defaults -and $job.defaults.run -and $job.defaults.run.shell) {
$jobShell = $job.defaults.run.shell
}
# 3. Iterate Steps
if ($job.steps) {
$stepIndex = 0
foreach ($step in $job.steps) {
$stepIndex++
# Skip if no 'run' key (e.g. uses: actions/checkout)
if (-not $step.run) { continue }
# Determine Step Shell (overrides job)
$stepShell = $jobShell
if ($step.shell) {
$stepShell = $step.shell
}
# Check if effective shell is PowerShell
# GitHub Actions accepts 'pwsh', 'powershell', or 'powershell {0}' custom formats
$isPwsh = $stepShell -match '^(pwsh|powershell)'
if ($isPwsh) {
$stepName = if ($step.name) { $step.name } else { "Step #$stepIndex" }
# Validate
$check = Test-PowerShellScript -ScriptText $step.run
if (-not $check.IsValid) {
foreach ($err in $check.Errors) {
$results += [PSCustomObject]@{
Job = $jobKey
Step = $stepName
Line = $err.Extent.StartLineNumber
Column = $err.Extent.StartColumnNumber
Message = $err.Message
Snippet = $step.run.Split("`n")[$err.Extent.StartLineNumber - 1]
}
}
}
}
}
}
}
}
return $results
}
#Usage
You can now use the Test-GhaPwshSteps function to validate your workflow files. You can iterate over all files in your repository to validate them all at once:
PowerShell
$files = @(
Get-ChildItem -Path ".github/workflows" -Include "*.yml", "*.yaml" -Recurse
Get-ChildItem -Path "." -Include "action.yml", "action.yaml" -Recurse
)
$errors = @()
foreach ($file in $files) {
$errors += Test-GhaPwshSteps -Path $file.FullName
}
if ($errors.Count -eq 0) {
Write-Host "No PowerShell syntax errors found in GitHub Actions workflows."
} else {
Write-Host "PowerShell syntax errors found in GitHub Actions workflows:"
$errors | Format-Table -AutoSize
}
This script won't catch all possible errors (like logic errors or runtime issues), but it is very effective at catching syntax errors such as missing braces, invalid keywords, or malformed strings before you push your code.
Do you have a question or a suggestion about this post? Contact me!