Using PowershellGet/PackageManagement with your own TFS2017 Package feed

In this blog post I’m going to demonstrate how you can use TFS2017’s Package feed functionality to host your own Powershell modules, how you can use PowershellGet to publish modules to this feed, and how to search, download and install modules from there.

Introduction

Since version 5, Powershell includes a package manager called PackageManagement, that allows you to search, download and install Powershell modules and scripts from a variety of sources in a unified manner.
Of these, the Powershell Gallery is probably the most well-known, but since it is backed (amongst others) by the NuGet infrastructure you can easily create and host your own package feed, for example to create a corporate script or module repository.
This is especially useful if you’ve written your own Cmdlets or functions that you use on multiple machines, while at the same time adding new functionality to them; By hosting these from your own package feed you have a single place to store and version your modules, and all it takes is one or two Powershell lines to update any machine with the latest version of your modules.

In fact, TFS2017 provides a way to easily create and host your own NuGet package feeds, which means its also perfectly suitable to store your own Powershell scripts and modules in. In this post, I’ll use this to host our own packages in so that they can be used from PowershellGet/PackageManagement.

I’m using Powershell 5.1 in combination with TFS2017. Some Cmdlets require elevated access, so be sure to run the Powershell ISE with Administrator privileges.

Installing the NuGet Package provider

Powershell PackageManagement (previously called “OneGet”) is actually a package aggregator (see the architecture explanation here) and is able to work with many different packaging sources. In order for PowershellGet to work with NuGet feeds, it needs the NuGet package provider. Installing it is as easy as:

Install-PackageProvider NuGet

In turn, it needs the NuGet client, so when installing the package provider it will offer to download and install the NuGet.exe if it is not already available. Alternatively, you can download the latest version
of NuGet from here and add its installation path to the Path environment variable so that the package provider can find it.

Updating the modules

Although Powershell 5 ships with the PowershellGet and PackageManagement modules, these are likely outdated and are missing important features (such as support for Register-PSRepository’s -Credential argument).
The latest versions of these modules are hosted on the Powershell gallery, so we can use PowershellGet to update itself; by default Install-Module will get the latest available version of a module.

There is a ‘gotcha’ here: because the current versions of these modules were not installed using PowershellGet, Install-Module will refuse to remove the old versions of these modules. But by specifying the -Force switch, you can tell it to perform a side-by-side installation, so that both the old and the new version are available. By default, the module is installed to %systemdrive%:Program FilesWindowsPowerShellModules:

Install-Module PackageManagement -Force
Install-Module PowershellGet -Force

By the way, Install-Module can tell the difference between pre-installed modules and the modules it has installed itself because in the latter, it adds a hidden PSGetModuleInfo.xml file to the module directory.

Ensuring the correct versions are loaded

Note that there are now multiple versions of these modules available:

Get-Module -ListAvailable | Where-Object { $_.Name -match '(PowershellGet|PackageManagement)' }

Because of this, we unload any current versions of these modules, and reload the latest versions we just installed. Note that Powershell does not always load the module with the highest version, so you might want to explicitly specify the -Version with Import-Module and load PackageManagement first (PowershellGet depends on PackageManagement, which could cause the wrong version to be selected):

Remove-Module PowershellGet
Remove-Module PackageManagement
Import-Module PackageManagement -Version 1.1.3.0
Import-Module PowershellGet -Version 1.1.3.1

Get-Module should now report these latest versions:

PS C:> Get-Module

ModuleType Version    Name               ExportedCommands
---------- -------    ----               ----------------
...
Script     1.1.3.0    PackageManagement  {Find-Package, Find-PackageProvider, ...
Script     1.1.3.1    PowershellGet      {Find-Command, Find-DscResource, Find...

We are now ready to register a repository.

Creating a TFS2017 Package feed

Lets create a new Package feed to host our Powershell modules in. In TFS2017, under the Build & Release menu, select Packages and create a new feed, and ensure that you’re allowed to push packages to it:

Next, click on the “Connect to feed” button and copy the url that’s displayed there, it should be something along the lines of:

https://tfsserver/tfs/DefaultCollection/_packaging/MyPowershellModules/nuget/v3/index.json

However, as you can see this is a NuGet v3 url, whereas PowershellGet (currently) only supports v2 endpoints. Luckily, it is easy to construct the v2 endpoint from this, simply by replacing “v3/index.json” with “v2“:

https://tfsserver/tfs/DefaultCollection/_packaging/MyPowershellModules/nuget/v2

Registering the feed

In order to use this feed with PowershellGet, it must be registered. Note that Register-PSRepository requires two separate url arguments, a -SourceLocation (where packages are retrieved from) and a -PublishLocation (where new package versions should be uploaded to). For the TFS2017 package feed, we can use the same v2 endpoint for both.
Also, we should specify if the feed is Trusted, or if we should provide confirmation every time a module is installed from this feed.
(N.B. If registering fails, check out the “The …is an invalid Web Uri error” section at the end of this post.)

$packageFeedUrl = "https://tfsserver/tfs/DefaultCollection/_packaging/MyPowershellModules/nuget/v2"
Register-PSRepository -Name "MyFeed" -SourceLocation $packageFeedUrl -PublishLocation $packageFeedUrl -InstallationPolicy Trusted

By default, the credentials of the user executing the script will also be used for authenticating with TFS. If you need a different set of credentials (maybe because you’re connecting from a different Active Directory domain), you can specify these with the -Credential argument (if this argument is not recognized, you’re using an old version of PowershellGet):

$cred = Get-Credential -Message "Enter the credentials to access TFS with" -UserName "MyDomainLeon"
Register-PSRepository -Name "MyFeed" -SourceLocation $packageFeedUrl -PublishLocation $packageFeedUrl -InstallationPolicy Trusted -Credential $cred  

If successful, Get-PSRepository should report our own feed, next to the default PSGallery that we used to update the PowershellGet and PackageManagement modules with:

PS C:> Get-PSRepository

Name       InstallationPolicy   SourceLocation
----       ------------------   --------------
MyFeed     Trusted              https://tfsserver/tfs/DefaultCollection/_packaging/MyPowershellModules/nuget/v2
PSGallery  Untrusted            https://www.powershellgallery.com/api/v2/

Uploading modules to the feed

Because powershell modules are already versioned (by the module’s .psd1 file) and are packaged by automatically PowershellGet, we can directly upload any module that is accessible via one of the directories in $env:PSModulePath.

For demo purposes, if you don’t have a module handy, you can first download one from the PSGallery, e.g. the Invoke-MsBuild module:

Install-Module "Invoke-MsBuild" -Repository "PSGallery"

Now that we have our sample module, we can use Publish-Module to upload it. Note that you need to specify to which feed you want to upload it, otherwise it will default to the PSGallery:

Publish-Module -Name "SampleModule" -Repository "MyFeed" -NuGetApiKey VSTS

In case you’re wondering about the -NuGetApiKey VSTS part: In the NuGet world, not everyone can ‘push’ (i.e. publish) packages to a NuGet feed, you need to specify a secret API key to do so. However, since we’re using a feed that is hosted by TFS2017, we instead specify the special value “VSTS” as the API Key, and let TFS2017 use our Windows/Domain credentials to determine if we’re allowed to push to this feed.
N.B. Apparently Publish-Module currently doesn’t handle the -Credential argument correctly, so make sure that you’re executing this cmdlet as a recognised TFS user.

After this, the module should have been uploaded to our own package feed. You can see this on TFS’ Package page, but also by querying the feed from Powershell:

PS C:> Find-Module -Repository "MyFeed"

Version    Name             Repository   Description
-------    ----             ----------   -----------
1.2.0      SampleModule     MyFeed       My sample module

Updating a module

Note that if we now try to push this same module again, it will fail:

PS C:> Publish-Module -Name "SampleModule" -Repository "MyFeed" -NuGetApiKey VSTS

Publish-Module : The module 'SampleModule' with version '1.2.0' cannot be published as the current version '1.2.0' is already available in the repository 
'https://tfsserver/tfs/DefaultCollection/_packaging/MyPowershellModules/nuget/v2'.
At line:1 char:1
+ Publish-Module -Name "SampleModule" -Repository "MyFeed" -NuGetApiK ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Publish-Module], InvalidOperationException
    + FullyQualifiedErrorId : ModuleVersionIsAlreadyAvailableInTheGallery,Publish-Module

Clearly, this is by design. The reasoning behind this is that once a package has been made public this way other solutions may depend on it, so this specific version of the package may not change anymore, and therefore cannot be overwritten by pushing the same version again. Rather, if you want to make changes to the module, you should give it a new version number and push that instead.

Say we have made some changes to our local version of this module and we want to increment the version number to reflect this, we should modify:

1. The version number included in the module path (e.g. C:Program FilesWindowsPowerShellModulesSampleModule1.2.0)
2. The ModuleVersion value in the module manifest (i.e. the SampleModule.psd1 file in that directory)

Now the Publish-Module will succeed, pushing the new version of the module to our package feed. If you run

Find-Module -Repository "MyFeed"

it may seem that only this new version is now available, but it’s still possible to acquire the older versions, as can be seen by:

PS C:> Find-Module -Repository "MyFeed" -Name "SampleModule" -AllVersions

Version    Name             Repository   Description
-------    ----             ----------   -----------
1.2.1      SampleModule     MyFeed       My sample module
1.2.0      SampleModule     MyFeed       My sample module

As we’ve seen, by default Install-Module will always take the highest available version of a module, but you can tell it to get a specific version as well:

Install-Module -Name "SampleModule" -Repository "MyFeed" -RequiredVersion "1.2.0"

The “…is an invalid Web Uri” error

During experimenting, I regularly encountered the following error:

Register-PSRepository : The specified Uri 'https://tfsserver/_packaging/MyPowershellModules/nuget/v2' for parameter 'SourceLocation' is an invalid Web Uri. 
Please ensure that it meets the Web Uri requirements.

Rather confusingly, I found that this could mean a variety of things:
– The specified url cannot be reached,
– The url it is not a valid NuGet v2 endpoint
– The specified -Credentials are invalid, or
– The credentials are valid, but TFS2017 doesn’t grant them access.

Should you encounter this error, be sure to check all the above cases. I found that a good way to tell what’s going on is to do a HTTP request to the endpoint directly:

Invoke-WebRequest -Uri $packageFeedUrl -Credential $cred

Conclusion

Creating your own package feed and using it to distribute your modules with isn’t too difficult once you’ve seen a couple of the required Cmdlets in action. Enjoy 🙂