Skip to content

Instantly share code, notes, and snippets.

@Jaykul
Last active November 26, 2019 15:46
Show Gist options
  • Save Jaykul/6786455981341e1b009fe20a79a60df4 to your computer and use it in GitHub Desktop.
Save Jaykul/6786455981341e1b009fe20a79a60df4 to your computer and use it in GitHub Desktop.
Making Publish-Module work with VisualStudio.com

This is a simple workaround solution for a complicated problem with registering VisualStudio online NuGet packages repositories as PowerShell module repositories.

The Problem with PowerShellGet

PowerShellGet just doesn't work with NuGet CredentialProviders, because it uses the -noninteractive switch whenever it calls nuget. However, the VisualStudio.com (VSO) nuget package repositories can work as internal PowerShell Galleries, because passing the credentials to Find-Module and Install-Module does work ...

Basically, the problem is two fold: authentication when publishing, and authentication when searching or installing. The second problem can be dealt with, inconveniently, by passing credentials every time. But the first cannot. These VSO repositories require a Credential Provider plugin to handle Microsoft Authentication (including support for 2-factor auth). This version of the credential provider works with nuget.exe (there's a different version for Visual Studio), which is what we want, because PowerShellGet just wraps the nuget.exe executable for publishing...

When publishing a module, PowerShellGet calls nuget push -noninteractive, so the Microsoft Visual Studio Team Services Authentication credential provider doesn't work at that point -- you just get an error saying it can't prompt in non-interactive mode. The work-around for that is that you can get a Personal Access Token (PAT), and set that key as the credential for the repository when you register it. Frustratingly, PowerShellGet doesn't help us with that workaround, because it uses the Package Management NuGet provider (which is download-only) for everything except publishing, and it doesn't ever call nuget.exe when you register a PSRepository. It only registers it with the PackageManagement NuGet provider, which doesn't appear to work with these credential providers.

The workaround

The script here basically handles both fetching the VSO credential manager, and putting it in the right place, and registering your VSO repository with the PAT (API key). This solves the publishing problem, but not the searching problem -- your credentials still won't be cached for searching.

The process is pretty simple:

  1. Get a personal access token that allows "Packaging (read and write)" or "Packaging (read, write, and manage)" -- note the "Expires In" setting, because you're going to have to do this over again every time the token expires.
  2. Run Set-VsoNuget, specifying the URL and Name for your repository, and the credentials, where the password in the personal access token from the first step.
<#
.Synopsis
Sets up the VisualStudio Online nuget feed and updates the credentials
.Description
To set up your Visual Studio online or VSTS repository as a PowerShell source,
you must first acquire a personal access token from VSTS (see http://bit.ly/VstsTokenInstructions),
and then call this script with the Url, Name, and a Credential which includes that token as the password.
.Example
Set-VsoNuGet -Url https://poshcode.pkgs.visualstudio.com/_packaging/PSModules/nuget/v2 -Name PoshCode -Cred (Get-Credential VsoAccessToken)
Configures my personal PoshCode gallery, prompting interactively for credentials.
.Example
$ApiKey = Read-Host "VSO API Key" -AsSecureString
Set-VsoNuGet -Credential ([PSCredential]::New("ignored", $ApiKey)) -Url https://poshcode.pkgs.visualstudio.com/_packaging/PSModules/nuget/v2 -Name PoshCode
Shows how to securely provide the API key at the console without a popup.
#>
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","CredentialProvider")]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","CredentialProviderPackage")]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword","CredentialProviderSource")]
[CmdletBinding()]
param(
# The URL to the NuGet repository to register for PowerShell must be a v2 url
# If you're using a VisualStudio.com URL that ends in /v3/packages.json, replace that with /v2
[Parameter(Mandatory=$true)]
[Uri]$UrlForRepository,
# The name of the repository to register
[Parameter(Mandatory=$true)]
[string]$Name,
# The credential must have the API key as the "password"
[System.Management.Automation.CredentialAttribute()]
[Parameter(Mandatory=$true)]
[PSCredential]$Credential,
# The source for installing the Credential provider
# Defaults to https://nuget.org/api/v2
[Parameter()]
[string]$CredentialProviderSource = "https://nuget.org/api/v2",
# The name of the Credential provider package.
# Defaults to Microsoft.VisualStudio.Services.NuGet.CredentialProvider
[Parameter()]
[string]$CredentialProviderPackage = "Microsoft.VisualStudio.Services.NuGet.CredentialProvider",
# The name of the Credential provider executable
# Defaults to CredentialProvider.VSS.exe
[Parameter()]
[string]$CredentialProvider = "CredentialProvider.VSS.exe",
# If set, makes the CredentialProvider global for all nuget.exe instances
[switch]$Global
)
$ErrorActionPreference = "Stop"
# A hack to find NuGet.exe so we can `nuget sources add` to store the credentials
$NuGetExe = &(Import-Module PowerShellGet -PassThru) { param($Caller)
if (!($script:NuGetExePath -and (Microsoft.PowerShell.Management\Test-Path -Path $script:NuGetExePath))) {
Install-NuGetClientBinaries -CallerPSCmdlet $Caller -BootstrapNuGetExe -Force
}
$script:NuGetExePath
} $PSCmdlet
$tempDirectory = "$env:temp\Packages-$(Get-Date -format 'yyyy-MM-dd_hh-mm-ss')"
$null = New-Item $tempDirectory -ItemType Directory -Force
# If not Global. install the credential provider where PowerShell's Nuget.exe is
$CredentialDirectory = if ($Global) {
"$Env:LocalAppData\NuGet\CredentialProviders"
} else {
Split-Path $NuGetExe
# "$Env:LocalAppData\Microsoft\Windows\PowerShell\PowerShellGet"
}
$null = New-Item $CredentialDirectory -ItemType Directory -Force
# Install the Credential Provider
$Null = Install-Package $CredentialProviderPackage -ProviderName NuGet -Source $CredentialProviderSource -Destination $TempDirectory -Force -ForceBootstrap -AllowPrereleaseVersions
if ($FoundItem = Get-ChildItem -Filter $CredentialProvider -Path $tempDirectory -Recurse) {
Copy-Item -Path $FoundItem.FullName -Destination $CredentialDirectory
} else {
Write-Warning "Failed to fetch updated $CredentialProvider from $CredentialProviderSource"
if (!(Test-Path (Join-Path $CredentialDirectory $CredentialProvider))) {
throw "$CredentialProviderPackage was not found at $CredentialProviderSource"
} else {
Write-Warning "$CredentialProvider may be out of date"
}
}
# Clean up after ourselves
Remove-Item $tempDirectory -Force -Recurse -ErrorAction SilentlyContinue
# If there's a previous repository, we can't re-register, so clean up the previous:
foreach($repo in Get-PSRepository | Where { $_.Name -eq $Name -or $_.SourceLocation -eq $UrlForRepository }) {
Unregister-PSRepository -Name $repo.Name
&$NuGetExe sources remove -name $repo.Name
}
# And now register the repository twice: once for PowerShell, and a second time to store the credentials
Register-PSRepository -InstallationPolicy Trusted -Name $Name -SourceLocation $UrlForRepository -PublishLocation $UrlForRepository
&$NuGetExe sources add -name $Name -source $UrlForRepository -username $Credential.UserName -password $Credential.GetNetworkCredential().Password
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment