Skip to main content
Cache PowerShell Modules in GitHub Actions
  1. Posts/

Cache PowerShell Modules in GitHub Actions

·909 words·5 mins·
Automation PowerShell GitHub Actions
Hudson McNamara
Author
Hudson McNamara
System Engineer
Table of Contents

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:

  1. Install Location - Depending on the scope of installation, the install location will differ.
  2. Operating System - The location of the modules will differ depending on the OS.
  3. 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.

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.

Don’t use -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!

If you run a workflow with debug logging enabled, it will tell you exactly what was restored and saved to/from cache!

Still confused or curious? Checkout the demo repository for a working example of everything above.

References
#

Related

Extract any Lenovo .INF Drivers
·191 words·1 min
Windows Admin Drivers
Extracting Lenovo driver files from their .exe packages.
Why Bluesky's ATProtocol Sucks
·977 words·5 mins
Decentralization Social Media Opinion
Opinion on the flaws of Bluesky & ATProtocol in practice.
Redirect Print Queue to a Different Queue
·122 words·1 min
Windows Printers
Redirect a Windows Printer queue to a different port.