Skip to main content

GitHub Actions

GitHub Actions is a popular CI/CD platform that integrates seamlessly with GitHub repositories. This guide shows you how to run psake builds in GitHub Actions workflows, including cross-platform matrix builds, secret management, and artifact publishing.

Quick Start

Here's a basic GitHub Actions workflow that runs a psake build:

name: Build

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]

jobs:
build:
runs-on: windows-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Run psake build
shell: pwsh
run: |
Install-Module -Name psake -Scope CurrentUser -Force
Invoke-psake -buildFile .\psakefile.ps1 -taskList Build

Installing psake in GitHub Actions

There are several approaches to installing psake in your workflow:

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

This is the simplest approach and works on all platforms (Windows, Linux, macOS).

Option 2: Install Specific Version

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

Option 3: Using requirements.psd1 with PSDepend

If your project uses a requirements.psd1 file:

- name: Install dependencies
shell: pwsh
run: |
Install-Module -Name PSDepend -Scope CurrentUser -Force
Invoke-PSDepend -Path ./requirements.psd1 -Install -Force

Your requirements.psd1:

@{
psake = @{
Version = '4.9.0'
}
# Other dependencies...
}

Option 4: Cache psake Installation

To speed up builds, cache the PowerShell modules directory:

- name: Cache PowerShell modules
uses: actions/cache@v4
with:
path: |
~/.local/share/powershell/Modules
~/Documents/PowerShell/Modules
key: ${{ runner.os }}-psake-${{ hashFiles('**/requirements.psd1') }}
restore-keys: |
${{ runner.os }}-psake-

- name: Install psake
shell: pwsh
run: |
if (-not (Get-Module -ListAvailable -Name psake)) {
Install-Module -Name psake -Scope CurrentUser -Force
}

Complete Workflow Example

Here's a comprehensive workflow demonstrating psake integration:

name: psake Build

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
workflow_dispatch:

env:
BUILD_CONFIGURATION: Release

jobs:
build:
runs-on: windows-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for versioning

- name: Setup PowerShell modules cache
uses: actions/cache@v4
with:
path: ~/.local/share/powershell/Modules
key: ${{ runner.os }}-psake-modules-${{ hashFiles('**/requirements.psd1') }}

- name: Install psake and dependencies
shell: pwsh
run: |
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-Module -Name psake -Scope CurrentUser -Force
Install-Module -Name PSDepend -Scope CurrentUser -Force

if (Test-Path ./requirements.psd1) {
Invoke-PSDepend -Path ./requirements.psd1 -Install -Force
}

- name: Run psake build
shell: pwsh
run: |
Invoke-psake -buildFile .\psakefile.ps1 `
-taskList Build, Test `
-parameters @{
Configuration = $env:BUILD_CONFIGURATION
BuildNumber = $env:GITHUB_RUN_NUMBER
}
env:
# Pass secrets as environment variables
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}

- name: Check build result
shell: pwsh
run: |
if (-not $?) {
Write-Error "psake build failed"
exit 1
}

- name: Upload build artifacts
uses: actions/upload-artifact@v4
if: success()
with:
name: build-output
path: |
./build/
./dist/
retention-days: 7

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: ./TestResults/
retention-days: 30

Cross-Platform Matrix Builds

psake works on Windows, Linux, and macOS. Use matrix builds to test across platforms:

name: Cross-Platform Build

on: [push, pull_request]

jobs:
build:
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
powershell-version: ['7.2', '7.4']
include:
- os: windows-latest
powershell-version: '5.1'

runs-on: ${{ matrix.os }}

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup PowerShell ${{ matrix.powershell-version }}
if: matrix.powershell-version != '5.1'
uses: PowerShell/setup-powershell@v1
with:
powershell-version: ${{ matrix.powershell-version }}

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

- name: Display environment info
shell: pwsh
run: |
Write-Host "OS: ${{ matrix.os }}"
Write-Host "PowerShell Version: $($PSVersionTable.PSVersion)"
Write-Host "psake Version: $((Get-Module -ListAvailable psake).Version)"

- name: Run psake build
shell: pwsh
run: |
Invoke-psake -buildFile .\psakefile.ps1 -taskList Build, Test

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.os }}-ps${{ matrix.powershell-version }}
path: ./build/

Secret Management

GitHub Actions provides a secure way to store and use secrets in your workflows.

Setting Up Secrets

  1. Go to your repository SettingsSecrets and variablesActions
  2. Click New repository secret
  3. Add secrets like NUGET_API_KEY, SIGNING_CERT_PASSWORD, etc.

Using Secrets in psake

Method 1: Environment Variables

- name: Run psake with secrets
shell: pwsh
run: Invoke-psake -buildFile .\psakefile.ps1 -taskList Deploy
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
AZURE_CONNECTION_STRING: ${{ secrets.AZURE_CONNECTION_STRING }}

In your psakefile.ps1:

Task Deploy {
$apiKey = $env:NUGET_API_KEY
if ([string]::IsNullOrEmpty($apiKey)) {
throw "NUGET_API_KEY environment variable is not set"
}

# Use the API key
dotnet nuget push "*.nupkg" --api-key $apiKey --source https://api.nuget.org/v3/index.json
}

Method 2: Parameters

- name: Run psake with secrets
shell: pwsh
run: |
Invoke-psake -buildFile .\psakefile.ps1 `
-taskList Deploy `
-parameters @{
NuGetApiKey = $env:NUGET_API_KEY
Environment = 'Production'
}
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}

In your psakefile.ps1:

Properties {
$NuGetApiKey = $null
$Environment = 'Development'
}

Task Deploy -precondition { $Environment -eq 'Production' } {
if ([string]::IsNullOrEmpty($NuGetApiKey)) {
throw "NuGetApiKey parameter is required for deployment"
}

dotnet nuget push "*.nupkg" --api-key $NuGetApiKey
}

Security Best Practices

  • Never hardcode secrets in your psakefile.ps1 or commit them to source control
  • Use environment-specific secrets for different deployment targets
  • Limit secret access to specific environments or branches using environment protection rules
  • Rotate secrets regularly and use GitHub's secret scanning to detect exposed secrets

Artifact Publishing

Publishing Build Artifacts

- name: Build with psake
shell: pwsh
run: Invoke-psake -buildFile .\psakefile.ps1 -taskList Build

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-${{ github.run_number }}
path: |
./build/**/*.dll
./build/**/*.exe
./build/**/*.nupkg
if-no-files-found: error
retention-days: 7

Publishing NuGet Packages

- name: Build and pack
shell: pwsh
run: Invoke-psake -buildFile .\psakefile.ps1 -taskList Build, Pack

- name: Publish to NuGet
if: github.ref == 'refs/heads/main'
shell: pwsh
run: |
Invoke-psake -buildFile .\psakefile.ps1 `
-taskList Publish `
-parameters @{ NuGetApiKey = $env:NUGET_API_KEY }
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}

Publishing to GitHub Releases

- name: Build release
shell: pwsh
run: |
Invoke-psake -buildFile .\psakefile.ps1 `
-taskList Build `
-parameters @{
Configuration = 'Release'
Version = $env:GITHUB_REF_NAME
}

- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v1
with:
files: |
./build/*.zip
./build/*.nupkg
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Example Repository Structure

Here's a recommended repository structure for using psake with GitHub Actions:

my-project/
├── .github/
│ └── workflows/
│ ├── build.yml # Main build workflow
│ ├── release.yml # Release workflow
│ └── pr-validation.yml # Pull request checks
├── src/
│ └── MyProject/
│ └── MyProject.csproj
├── tests/
│ └── MyProject.Tests/
│ └── MyProject.Tests.csproj
├── build/ # Build output directory
├── psakefile.ps1 # Main build script
├── requirements.psd1 # PowerShell dependencies
└── README.md

psakefile.ps1 example:

Properties {
$Configuration = 'Debug'
$BuildNumber = '0'
$Version = "1.0.$BuildNumber"
$SrcDir = Join-Path $PSScriptRoot 'src'
$TestDir = Join-Path $PSScriptRoot 'tests'
$BuildDir = Join-Path $PSScriptRoot 'build'
}

Task Default -depends Build, Test

Task Clean {
if (Test-Path $BuildDir) {
Remove-Item $BuildDir -Recurse -Force
}
New-Item -ItemType Directory -Path $BuildDir | Out-Null
}

Task Build -depends Clean {
exec { dotnet build $SrcDir -c $Configuration -o $BuildDir /p:Version=$Version }
}

Task Test -depends Build {
exec { dotnet test $TestDir -c $Configuration --no-build }
}

Task Pack -depends Build {
exec { dotnet pack $SrcDir -c $Configuration -o $BuildDir /p:Version=$Version }
}

Task Publish -depends Pack {
$apiKey = $env:NUGET_API_KEY
if ([string]::IsNullOrEmpty($apiKey)) {
throw "NUGET_API_KEY environment variable is required"
}

Get-ChildItem "$BuildDir/*.nupkg" | ForEach-Object {
exec { dotnet nuget push $_.FullName --api-key $apiKey --source https://api.nuget.org/v3/index.json }
}
}

Common Troubleshooting

psake Module Not Found

Problem: Import-Module: The specified module 'psake' was not loaded

Solution:

- name: Install psake with verbose output
shell: pwsh
run: |
Install-Module -Name psake -Scope CurrentUser -Force -Verbose
Get-Module -ListAvailable psake

PowerShell Execution Policy Issues

Problem: Scripts fail due to execution policy on Windows runners

Solution: Use pwsh shell (PowerShell Core) instead of powershell (Windows PowerShell):

- name: Run psake
shell: pwsh # Not 'powershell'
run: Invoke-psake

Build Failures Not Failing the Workflow

Problem: psake build fails but workflow shows success

Solution: Ensure psake errors exit with non-zero code:

# In your psakefile.ps1
FormatTaskName {
param($taskName)
Write-Host "Executing task: $taskName" -ForegroundColor Cyan
}

# Use exec for external commands
Task Build {
exec { dotnet build } # Will fail the build on non-zero exit code
}

Or check the result explicitly:

- name: Run psake build
shell: pwsh
run: |
Invoke-psake -buildFile .\psakefile.ps1
if (-not $?) {
exit 1
}

Path Issues on Cross-Platform Builds

Problem: Windows paths don't work on Linux/macOS

Solution: Use PowerShell's cross-platform path cmdlets:

Properties {
# Instead of: $BuildDir = "$PSScriptRoot\build"
$BuildDir = Join-Path $PSScriptRoot 'build'

# Instead of: $OutputPath = ".\build\bin"
$OutputPath = Join-Path (Join-Path $PSScriptRoot 'build') 'bin'
}

Module Caching Not Working

Problem: Cache doesn't restore modules correctly

Solution: Use platform-specific cache paths:

- name: Cache PowerShell modules
uses: actions/cache@v4
with:
path: |
${{ runner.os == 'Windows' && '~\Documents\PowerShell\Modules' || '~/.local/share/powershell/Modules' }}
key: ${{ runner.os }}-psake-${{ hashFiles('**/requirements.psd1') }}

Secrets Not Available in Forked PRs

Problem: Workflows from forked pull requests can't access secrets

Solution: This is a security feature. For PRs from forks:

  • Use pull_request_target event (be cautious - review code first)
  • Or manually trigger workflows after review
  • Or skip deployment tasks for untrusted PRs
- name: Deploy
if: github.event.pull_request.head.repo.full_name == github.repository
shell: pwsh
run: Invoke-psake -taskList Deploy

Advanced Patterns

Conditional Task Execution Based on Changed Files

- name: Detect changes
id: changes
uses: dorny/paths-filter@v3
with:
filters: |
src:
- 'src/**'
docs:
- 'docs/**'

- name: Build code
if: steps.changes.outputs.src == 'true'
shell: pwsh
run: Invoke-psake -taskList Build

- name: Build docs
if: steps.changes.outputs.docs == 'true'
shell: pwsh
run: Invoke-psake -taskList BuildDocs

Reusable Workflows

Create .github/workflows/psake-build.yml as a reusable workflow:

name: Reusable psake Build

on:
workflow_call:
inputs:
task-list:
required: true
type: string
configuration:
required: false
type: string
default: 'Release'
secrets:
nuget-api-key:
required: false

jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

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

- name: Run psake
shell: pwsh
run: |
Invoke-psake -taskList $env:TASK_LIST `
-parameters @{ Configuration = $env:CONFIGURATION }
env:
TASK_LIST: ${{ inputs.task-list }}
CONFIGURATION: ${{ inputs.configuration }}
NUGET_API_KEY: ${{ secrets.nuget-api-key }}

Use it in other workflows:

name: Main Build

on: [push]

jobs:
build:
uses: ./.github/workflows/psake-build.yml
with:
task-list: 'Build, Test'
configuration: 'Release'

See Also