I am a big fan of modules as a way to package and distribute PowerShell functions. I create modules all the time and I tend to use a fairly robust set of build and release scripts. More recently, I have wanted to release an individual advanced function as a module but I felt that my existing process was a bit much. So I started building micro modules instead.

Index

What is a micro module?

A micro module is very small in scope and often has a single function. Building a micro module is about getting back to the basics and keeping everything as simple as possible.

There is a lot of good advice out there on how to build a module. That guidance is there to assist you as your module grows in size. If we know that our module will not grow and we will not add any functions, we can take a different approach even though it may not conform fully to the community best practices.

Single function

The whole idea behind the micro module is that there is only one function. Because there is one function, we place it directly into the .psm1 file in source. This is important because when you have multiple functions, you should have them in their own files.

I love having multiple files for my dev work but that requires that I add a build script to combine them into the .psm1 for publishing. By having the single function in the .psm1 file already, I can skip that build step. I can just publish the module as it sits in source control.

A closer look

Let’s take a look at the structure of my Watch-Command module. Watch-Command is a micro module that I published last week. I see 7 files in this project.

Watch-Command
│   azure-pipelines.yml
│   publish.ps1
│   readme.md
│
├───.vscode
│       settings.json
│
└───Watch-Command
        LICENSE
        Watch-Command.psd1
        Watch-Command.psm1

Module files

The Watch-Command folder is the actual module with 3 files. The .psm1 file has 90 lines of PowerShell for the 1 function (named the same as my module). The .psd1 is still important and is required to publish to the PSGallery. I also have a LICENSE file in this folder so it gets delivered with the module.

Continuous Delivery

Even though it is a simple module, I still leverage a continuous delivery pipeline to publish the module.

publish.ps1

At the heart of the pipeline is the publish.ps1 that will publish my module to the PSGallery. Here is a look at the publish.ps1 file in the project.

$publishModuleSplat = @{
    Path        = ".\Watch-Command"
    NuGetApiKey = $ENV:nugetapikey
    Verbose     = $true
    Force       = $true
    Repository  = "PSGallery"
    ErrorAction = 'Stop'
}

"Files in module output:"
Get-ChildItem $Destination -Recurse -File |
    Select-Object -Expand FullName

"Publishing [$Destination] to [$PSRepository]"

Publish-Module @publishModuleSplat

It is basically a call to Publish-Module with a little verbosity. The important detail here is that the nugetapikey is pulled from an environment variable.

azure-pipelines.yml

I am using azure devops pipelines to manage the deployment. I define the whole build in the azure-pipelines.yml file. This allows me to just point the pipeline at my source and the build will just work.

trigger:
  batch: true
  branches:
    include:
      - master

pool:
  vmImage: 'windows-2019'

steps:
- script: pwsh -Command {Install-Module PowerShellGet -Force}
  displayName: 'Update powershellget'
- script: pwsh -File publish.ps1
  displayName: 'Build and Publish Module'
  env:
    nugetapikey: $(nugetapikey)

The first thing I do is update PowerShellGet in its own build step. This way when the next PowerShell step executes, I know that it is running with a current verison of PowerShellGet.

For the second step, I call the publish.ps1 script to publish the module. I also need to map the environment nugetapikey to the build step or it will be null in my script for the publish.

Setting up the pipeline

I set up a single DevOps Pipeline for all my micro modules, but each one will get a unique build in that pipeline. When you create the build, you will have to point it at your source repository. Then specify the azure-pipelines.yml for the build definition.

nugetapikey

I generate a new PSGallery api key each project and add it to the build as an environment vaiable. Make sure you click the little lock to protect the value.

publish

At this point, I am able to merge into master and the module will get published.

Closing comments

If you get a chance, you should check out my Watch-Command module. It lets you specify a command to be ran every 15 seconds. It will then clear the screen and show you the results of that command (over and over until you kill it).

Here is an example of it showing the local process list sorted by cpu time.

Watch-Command {Get-Process | Sort cpu -desc}

This micro module pattern was fast to set up. I was able to write my Watch-Command module and have the pipeline publishing it the same day. This allowed me to focus on my ideas and quickly get it out the door.