In a PowerShell Module I was creating, I needed to cache a handful of modules in my GitHub Actions workflows – I couldn’t use some prebuilt actions, as my dependencies are centrally defined in a requirements.psd1
, then using PSDepend to facilitate installation, obviously my next best choice was to figure it out myself.
Prebuilt Actions? #
If you can use one for your situation, I would actually recommend using potatoqualitee/PSModuleCache where possible, it uses actions/cache
under the hood and does A LOT of the heavy-lifting for you (see Things to Consider below) including installation.
- name: Install/Cache Modules
uses: potatoqualitee/psmodulecache@v6.2
with:
modules-to-cache: PSFramework, PoshRSJob
However it explicitly requires you to re-write whatever modules you need to cache within the step itself. If you are using other means to track/install your dependencies (such as PSDepend or PSake) it is not ideal.
Things to Consider #
There are a few big items to consider when caching PowerShell modules:
- Install Location - Depending on the scope of installation, the install location will differ.
- Operating System - The location of the modules will differ depending on the OS.
- Preinstalled Modules - Hosted Runners comes with a set of preinstalled modules on the system level.
By default in GitHub Hosted Runners, modules are installed under the system-level module path. However, caching this is not ideal as it contains a lot of the preinstalled modules we mentioned earlier, most are large and more often than not, not relevant to the project.
In my case, I ended up forcing my modules to either a custom location or the user-level module path. You can use a custom location if you want to, as long as the PSModulePath
environment variable includes the custom location. Obviously, people working on the project will need to set this environment variable in their local environment as well.
- Checkout Chrissy LeMaire’s post for all
PSModulePath
’s on GitHub Runners, for all operating systems.
PSDepend Install #
You can force the installation location in your dependencies file with Target
set to CurrentUser
(or a custom path):
@{
'psake' = @{
Version = '4.9.1'
Target = 'CurrentUser'
}
#...
}
PSDepend can also add path entries to the PSModulePath
environment variable for you:
@{
PSDependOptions = @{
Target = '.\path\to\custom\location'
AddToPath = $true
}
'psake' = '4.9.1'
'PoshRSJob' = '3.0.0'
#...
}
Writing the Action #
Once you have the install location set, you can use actions/cache
to cache the modules. Take note of where the modules are installed, as you will need to set the path
parameter:
# Windows Only
- name: Cache PowerShell modules
uses: actions/cache@v4
with:
path: C:\Users\runneradmin\Documents\PowerShell\Modules
key: ${{ runner.os }}-pwsh-${{ hashFiles('**/requirements.psd1') }}
restore-keys: |
${{ runner.os }}-pwsh-
I’m using a requirements file as the cache key, so if I add or update something, it will invalidate my cache and re-install the modules (as the Hash will change).
Matrix Strategy #
For a Matrix, you’ll have to get creative to run on multiple OS’s – You could use the runner.os
variable to determine which you’re on, then set the path programmatically:
- name: Set cache path
run: |
if ('${{ runner.os }}' -eq 'Windows') {
echo "CACHE_PATH=$(Join-Path $HOME 'Documents\PowerShell\Modules')" >> $env:GITHUB_ENV
} else {
echo "CACHE_PATH=$HOME/.local/share/powershell/Modules" >> $env:GITHUB_ENV
}
- name: Cache PowerShell modules
uses: actions/cache@v4
id: cache
with:
path: ${{ env.CACHE_PATH }}
key: ${{ runner.os }}-pwsh-${{ hashFiles('**/requirements.psd1') }}
restore-keys: |
${{ runner.os }}-pwsh-
Install and Import #
When a cache key is restored, you don’t need to install the modules again, but you will need to import them. Conversely, if the cache is not restored, you’ll have to install the modules first, then import them.
-Force
when using Install-Module
in your workflow, as it will always reinstall modules regardless.
You can use an if
conditional to install or import depending on if cache was restored, since actions/cache
will set an output variable cache-hit
:
- name: Install Dependencies if not found
if: steps.cache.outputs.cache-hit != 'true'
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name PSDepend -Scope CurrentUser -Confirm:$false
Invoke-PSDepend -Install -Force -Verbose
- name: Import Modules
run: |
Import-Module -Name PSDepend -Verbose
Invoke-PSDepend -Import -Force -Verbose
Example Flow #
The example below shows a complete matrix workflow that caches PowerShell modules using actions/cache
and installs them if they are not found in the cache.
name: Example Matrix Workflow
on: push
defaults:
run:
shell: pwsh # ps7
jobs:
your-matrix-job:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set cache path
run: |
if ('${{ runner.os }}' -eq 'Windows') {
echo "CACHE_PATH=$(Join-Path $HOME 'Documents\PowerShell\Modules')" >> $env:GITHUB_ENV
} else {
echo "CACHE_PATH=$HOME/.local/share/powershell/Modules" >> $env:GITHUB_ENV
}
- name: Cache PowerShell modules
uses: actions/cache@v4
id: cache
with:
path: ${{ env.CACHE_PATH }}
key: ${{ runner.os }}-pwsh-${{ hashFiles('**/requirements.psd1') }}
restore-keys: |
${{ runner.os }}-pwsh-
- name: Install Dependencies if not found
if: steps.cache.outputs.cache-hit != 'true'
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name PSDepend -Scope CurrentUser -Confirm:$false
Invoke-PSDepend -Install -Force -Verbose
- name: Import Modules
run: |
Import-Module -Name PSDepend -Verbose
Invoke-PSDepend -Import -Force -Verbose
- name: Run Something
run: Invoke-PSake -NoLogo
What’s Next? #
Run your Action!
Still confused or curious? Checkout the demo repository for a working example of everything above.