Javy de Koning

Welcome

Geek 🤓, love sports 🏃‍♂️🏋️‍♂️, food 🍛,
tech , @ Amsterdam ❌❌❌.

Javy de Koning

6 minutes read

AppVeyor is a CI/CD (Continuous Integration / Continuous Deployment) platform that’s aimed at .NET developers. In this blog-post I’ll take you through the basics for setting up your first Project. We will be building, testing and deploying a PowerShell Module to the PowerShell Gallery using AppVeyor and GitHub.

Getting started

Before we go into the details you will need fork my AppVeyorDemo repository OR setup your own GitHub repository. Next sign-in and add your project. Every build runs on a fresh (currently Windows 2012r2 x64) virtual machine which is not shared with other builds and the state of which is not preserved between consequent builds. After the build is finished its virtual machine is decommissioned.

Build config (appveyor.yml)

Project builds can be configured by either YAML formatted file or using the AppVeyor user interface. I prefer to use YAML file because it makes it easier to fork/clone or contribute to a project. A minimal YAML is displayed below:

#---------------------------------# 
#      environment configuration  # 
#---------------------------------# 
version: 1.0.{build}
os: WMF 5
install:
  - ps: . .\AppVeyor\AppVeyorInstall.ps1

environment:
  MySecureVar:
    secure: 2miEAGV72ED32TvZbXUbQA== #This is secured
  MyNonSecureVar: NonSecure
  ModuleName: appVeyorDemo
#---------------------------------# 
#      build configuration        # 
#---------------------------------# 
build_script: 
  - ps: . .\AppVeyor\AppVeyorBuild.ps1

#---------------------------------# 
#      test configuration         # 
#---------------------------------# 
test_script: 
  - ps: . .\AppVeyor\AppVeyorTest.ps1

#---------------------------------# 
#      deployment configuration   # 
#---------------------------------# 
deploy_script: 
  - ps: . .\AppVeyor\AppveyorDeploy.ps1

There is not much we need to configure in the YAML file because most parts are handled in linked PowerShell scripts. Nevertheless lets go over the details:

  • version: this is the version number that the build will get. {build} is automatically incremented by AppVeyor with every build. You can change/reset this value in the Projects General settings page
  • os: the Build worker image to use. We will use ‘WMF 5’
  • environment: the environment variables to use. You can store both ‘secure’ and ‘non-secure’ variables here. You can encrypt a string here. The encrypted string can be stored in the yml file and only your account can decrypt that string in the build process. If another user tries to access it (for example in a pull request) it will not exist. From PowerShell we can access those variables using $env:var and we will do so later on.
  • Install, Build, Test, Deploy: we will handle all of these phases from PowerShell. These lines just execute the PowerShell scripts during the build process.

Install

As noted before, the ‘install’ phase will be handled from PowerShell. Because a new VM is launched for every build, we will need to set some prerequisites. In this example we are going to add the NuGet package provider and install the Pester and PSScriptAnalyzer modules from the PowerShell Gallery as we will require these modules for testing our own modules and/or scripts.

#---------------------------------# 
# Header                          # 
#---------------------------------# 
Write-Host 'Running AppVeyor install script' -ForegroundColor Yellow

#---------------------------------# 
# Install NuGet                   # 
#---------------------------------# 
Write-Host 'Installing NuGet PackageProvide'
$pkg = Install-PackageProvider -Name NuGet -Force
Write-Host "Installed NuGet version '$($pkg.version)'" 

#---------------------------------# 
# Install Pester                  # 
#---------------------------------# 
Write-Host 'Installing Pester'
Install-Module -Name Pester -Repository PSGallery -Force

#---------------------------------# 
# Install PSScriptAnalyzer        # 
#---------------------------------# 
Write-Host 'Installing PSScriptAnalyzer'
Install-Module PSScriptAnalyzer -Repository PSGallery -force

Build

Since we are deploying a simple PowerShell module there is nothing to build. We will just print some info here:

#---------------------------------# 
# Header                          # 
#---------------------------------# 
Write-Host 'Running AppVeyor build script' -ForegroundColor Yellow
Write-Host "ModuleName    : $env:ModuleName"
Write-Host "Build version : $env:APPVEYOR_BUILD_VERSION"
Write-Host "Author        : $env:APPVEYOR_REPO_COMMIT_AUTHOR"
Write-Host "Branch        : $env:APPVEYOR_REPO_BRANCH"

#---------------------------------# 
# BuildScript                     # 
#---------------------------------# 
Write-Host 'Nothing to build, skipping.....'

Successful Build

Test

Here is where our testing occurs. We will call the Pester tests from this script (line 11) and upload the results to the AppVeyor API (line 14). Using this approach has the benefit of including the test results as an XML in your repository. If any of the tests fail we will stop the build and will not continue with the deployment phase.

#---------------------------------# 
# Header                          # 
#---------------------------------# 
Write-Host 'Running AppVeyor test script' -ForegroundColor Yellow
Write-Host "Current working directory: $pwd"

#---------------------------------# 
# Run Pester Tests                # 
#---------------------------------# 
$testResultsFile = '.\TestsResults.xml'
$res             = Invoke-Pester -Script .\Tests\AppVeyorDemo.Tests.ps1 -OutputFormat NUnitXml -OutputFile $testResultsFile -PassThru

Write-Host 'Uploading results'
(New-Object 'System.Net.WebClient').UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $testResultsFile))

#---------------------------------# 
# Validate                        # 
#---------------------------------# 
if (($res.FailedCount -gt 0) -or ($res.PassedCount -eq 0)) { 
    throw "$($res.FailedCount) tests failed."
} else {
  Write-Host 'All tests passed' -ForegroundColor Green
}

Pester and PSScriptAnalyzer tests

A template Pester and PSScriptAnalyzer tests is shared below (Credits to Ryan Yates). You should be able to keep most of it. Make sure you create some unit tests for your module and replace the ‘custom’ part of the script below.

#---------------------------------# 
# PSScriptAnalyzer tests          # 
#---------------------------------# 
$Scripts = Get-ChildItem "$PSScriptRoot\..\" -Filter '*.ps1' -Recurse | Where-Object {$_.name -NotMatch 'Tests.ps1'}
$Modules = Get-ChildItem "$PSScriptRoot\..\" -Filter '*.psm1' -Recurse
$Rules   = Get-ScriptAnalyzerRule

if ($Modules.count -gt 0) {
  Describe 'Testing all Modules against default PSScriptAnalyzer rule-set' {
    foreach ($module in $modules) {
      Context "Testing Module '$($module.FullName)'" {
        foreach ($rule in $rules) {
          It "passes the PSScriptAnalyzer Rule $rule" {
            (Invoke-ScriptAnalyzer -Path $module.FullName -IncludeRule $rule.RuleName ).Count | Should Be 0
          }
        }
      }
    }
  }
}
if ($Scripts.count -gt 0) {
  Describe 'Testing all Script against default PSScriptAnalyzer rule-set' {
    foreach ($Script in $scripts) {
      Context "Testing Script '$($script.FullName)'" {
        foreach ($rule in $rules) {
          It "passes the PSScriptAnalyzer Rule $rule" {
            if (-not ($module.BaseName -match 'AppVeyor') -and -not ($rule.Rulename -eq 'PSAvoidUsingWriteHost') ) {
              (Invoke-ScriptAnalyzer -Path $script.FullName -IncludeRule $rule.RuleName ).Count | Should Be 0
            }
          }
        }
      }
    }
  }
}

#---------------------------------# 
# Custom Pester tests (replace)   # 
#---------------------------------# 

$PSVersion    = $PSVersionTable.PSVersion.Major
$AppVeyorDemo = "$PSScriptRoot\..\AppVeyorDemo.psm1"

Describe "AppVeyorDemo PS$PSVersion" {
    Copy-Item $AppVeyorDemo TestDrive:\script.ps1 -Force
    Mock Export-ModuleMember {return $true}
    . 'TestDrive:\script.ps1'

    Context 'Strict mode' { 
        Set-StrictMode -Version latest

        It 'Get-SomeInt should return int' {
          Get-SomeInt | Should BeOfType System.Int32
        }
    }
}

Successful Tests

Deploy

When all tests have passed we can continue with the deployment. Some things that we take care of here:

  1. Update the module manifest version. We use regex inject the ‘APPVEYOR_BUILD_VERSION’ environment variable that we configured in the appveyor.yml. (line 12)
  2. Verify the GitHub branch. If the GitHub branch is NOT the master branch we do not want to deploy to the PowerShell Gallery. (line 17)
  3. Adding the project path to the ‘psmodulepath’ PATH variable”. This is required for the ‘Publish Module’ cmdlet (line 25)
  4. Publish the module. Lastly we publish the module to the PowerShell Gallery. This requires your NuGetApiKey to be correctly configured encrypted and stored in the appveyor.yml file.
#---------------------------------# 
# Header                          # 
#---------------------------------# 
Write-Host 'Running AppVeyor deploy script' -ForegroundColor Yellow

#---------------------------------# 
# Update module manifest          # 
#---------------------------------# 
Write-Host 'Creating new module manifest'
$ModuleManifestPath = Join-Path -path "$pwd" -ChildPath ("$env:ModuleName"+'.psd1')
$ModuleManifest     = Get-Content $ModuleManifestPath -Raw
[regex]::replace($ModuleManifest,'(ModuleVersion = )(.*)',"`$1'$env:APPVEYOR_BUILD_VERSION'") | Out-File -LiteralPath $ModuleManifestPath

#---------------------------------# 
# Publish to PS Gallery           # 
#---------------------------------# 
if ($env:APPVEYOR_REPO_BRANCH -notmatch 'master')
{
    Write-Host "Finished testing of branch: $env:APPVEYOR_REPO_BRANCH - Exiting"
    exit;
}

$ModulePath = Split-Path $pwd
Write-Host "Adding $ModulePath to 'psmodulepath' PATH variable"
$env:psmodulepath = $env:psmodulepath + ';' + $ModulePath

Write-Host 'Publishing module to Powershell Gallery'
#Uncomment the below line, make sure you set the variables in appveyor.yml
#Publish-Module -Name $env:ModuleName -NuGetApiKey $env:NuGetApiKey

Successful Deploy

comments powered by Disqus

Recent posts

See more

Categories

About

There should go some text here but I'm to lazy to write it.