It may not be the most common thing, however it may happen that you would need to upload an artifact to a maven repository in Nexus via PowerShell. In order to achieve that, we will use Nexus REST API which for this task requires a multipart/form-data POST to /service/local/artifact/maven/content
resource. This is however not a trivial task. It is notoriously difficult to manage a Multipart/form-data standard in PowerShell, as I already described in one of my previous post PowerShell tips and tricks – Multipart/form-data requests. As you can see this type of call is necessary in order to upload an artifact to your Nexus server. I will not get in a details about Multipart/form-data requests and if interested about the details, you can check just mentioned post.
For this purpose I created two cmdlets that will allow me to upload an artifact by supplying GAV parameters or by passing the GAV definition in a POM file. A couple of simple functions do support both of these cmdlets.
GAV definition in a POM file
As the heading suggests, this cmdlet will let you upload your artifact and specify the GAV parameters via a POM file.
function Import-ArtifactPOM() { [CmdletBinding()] param ( [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$EndpointUrl, [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Repository, [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$PomFilePath, [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$PackagePath, [System.Management.Automation.PSCredential][parameter(Mandatory = $true)]$Credential ) BEGIN { if (-not (Test-Path $PackagePath)) { $errorMessage = ("Package file {0} missing or unable to read." -f $PackagePath) $exception = New-Object System.Exception $errorMessage $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, 'ArtifactUpload', ([System.Management.Automation.ErrorCategory]::InvalidArgument), $PackagePath $PSCmdlet.ThrowTerminatingError($errorRecord) } if (-not (Test-Path $PomFilePath)) { $errorMessage = ("POM file {0} missing or unable to read." -f $PomFilePath) $exception = New-Object System.Exception $errorMessage $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, 'ArtifactUpload', ([System.Management.Automation.ErrorCategory]::InvalidArgument), $PomFilePath $PSCmdlet.ThrowTerminatingError($errorRecord) } Add-Type -AssemblyName System.Net.Http } PROCESS { $repoContent = CreateStringContent "r" $Repository $groupContent = CreateStringContent "hasPom" "true" $pomContent = CreateStringContent "file" "$(Get-Content $PomFilePath)" ([system.IO.Path]::GetFileName($PomFilePath)) "text/xml" $streamContent = CreateStreamContent $PackagePath $content = New-Object -TypeName System.Net.Http.MultipartFormDataContent $content.Add($repoContent) $content.Add($groupContent) $content.Add($pomContent) $content.Add($streamContent) $httpClientHandler = GetHttpClientHandler $Credential return PostArtifact $EndpointUrl $httpClientHandler $content } END { } }
In order to invoke this cmdlet you will need to supply the following parameters:
- EndpointUrl – Address of your Nexus server
- Repository – Name of your repository in Nexus
- PomFilePath – Full, literal path pointing to your POM file
- PackagePath – Full, literal path pointing to your Artifact
- Credential – Credentials in the form of PSCredential object
I will create a POM file with the following content:
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.mycompany.app</groupId> <artifactId>my-app</artifactId> <version>3</version> </project>
And invoke my cmdlet in the following way:
$server = "http://nexus.maio.com" $repoName = "maven" $pomFile = "C:\Users\majcicam\Desktop\pom.xml" $package = "C:\Users\majcicam\Desktop\junit-4.12.jar" $credential = Get-Credential Import-ArtifactPOM $server $repoName $pomFile $package $credential
If everything is correctly setup, you will be first asked to provide the credentials, then the upload will start. If it succeeds, you will receive back a string like this:
{"groupId":"com.mycompany.app","artifactId":"my-app","version":"3","packaging":"jar"}
It is a json representation of the information about imported package.
Manually supplying GAV parameters
The other cmdlet is based on a similar principle, however it doesn’t require a POM file to be passed in, instead it let you provide GAV values as parameter to the call.
function Import-ArtifactGAV() { [CmdletBinding()] param ( [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$EndpointUrl, [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Repository, [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Group, [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Artifact, [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Version, [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Packaging, [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$PackagePath, [System.Management.Automation.PSCredential][parameter(Mandatory = $true)]$Credential ) BEGIN { if (-not (Test-Path $PackagePath)) { $errorMessage = ("Package file {0} missing or unable to read." -f $packagePath) $exception = New-Object System.Exception $errorMessage $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, 'XLDPkgUpload', ([System.Management.Automation.ErrorCategory]::InvalidArgument), $packagePath $PSCmdlet.ThrowTerminatingError($errorRecord) } Add-Type -AssemblyName System.Net.Http } PROCESS { $repoContent = CreateStringContent "r" $Repository $groupContent = CreateStringContent "g" $Group $artifactContent = CreateStringContent "a" $Artifact $versionContent = CreateStringContent "v" $Version $packagingContent = CreateStringContent "p" $Packaging $streamContent = CreateStreamContent $PackagePath $content = New-Object -TypeName System.Net.Http.MultipartFormDataContent $content.Add($repoContent) $content.Add($groupContent) $content.Add($artifactContent) $content.Add($versionContent) $content.Add($packagingContent) $content.Add($streamContent) $httpClientHandler = GetHttpClientHandler $Credential return PostArtifact $EndpointUrl $httpClientHandler $content } END { } }
In order to invoke this cmdlet you will need to supply the following parameters:
- EndpointUrl – Address of your Nexus server
- Repository – Name of your repository in Nexus
- Group – Group Id
- Artifact – Maven artifact Id
- Version – Artifact version
- Packaging – Packaging type (at ex. jar, war, ear, rar, etc.)
- PackagePath – Full, literal path pointing to your Artifact
- Credential – Credentials in the form of PSCredential object
An example of invocation:
$server = "http://nexus.maio.com" $repoName = "maven" $group = "com.test" $artifact = "project" $version = "2.4" $packaging = "jar" $package = "C:\Users\majcicam\Desktop\junit-4.12.jar" $credential = Get-Credential Import-ArtifactGAV $server $repoName $group $artifact $version $packaging $package $credential
If all goes as expected you will again receive a response confirming the imported values.
{"groupId":"com.test","artifactId":"project","version":"2.4","packaging":"jar"}
Supporting functions
As you could see, my cmdlets relay on a couple of functions. They are essential in this process so I will analyse them one by one.
function CreateStringContent() { param ( [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Name, [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Value, [string]$FileName, [string]$MediaTypeHeaderValue ) $contentDispositionHeaderValue = New-Object -TypeName System.Net.Http.Headers.ContentDispositionHeaderValue -ArgumentList @("form-data") $contentDispositionHeaderValue.Name = $Name if ($FileName) { $contentDispositionHeaderValue.FileName = $FileName } $content = New-Object -TypeName System.Net.Http.StringContent -ArgumentList @($Value) $content.Headers.ContentDisposition = $contentDispositionHeaderValue if ($MediaTypeHeaderValue) { $content.Headers.ContentType = New-Object -TypeName System.Net.Http.Headers.MediaTypeHeaderValue $MediaTypeHeaderValue } return $content } function CreateStreamContent() { param ( [string][parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$PackagePath ) $packageFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList @($PackagePath, [System.IO.FileMode]::Open) $contentDispositionHeaderValue = New-Object -TypeName System.Net.Http.Headers.ContentDispositionHeaderValue "form-data" $contentDispositionHeaderValue.Name = "file" $contentDispositionHeaderValue.FileName = Split-Path $packagePath -leaf $streamContent = New-Object -TypeName System.Net.Http.StreamContent $packageFileStream $streamContent.Headers.ContentDisposition = $contentDispositionHeaderValue $streamContent.Headers.ContentType = New-Object -TypeName System.Net.Http.Headers.MediaTypeHeaderValue "application/octet-stream" return $streamContent }
First two functions are there to create a correct HTTP content. Each content object we create will correspond to a content-disposition header. The first function will return a string type values for the just mentioned header, meanwhile the Stream content will return a stream that will be consumed later on, having as a value octet-stream representation of our artifact.
GetHttpClientHandler
function is a helper that will create the right http client handler that contains the credentials to be used for our call.
function GetHttpClientHandler() { param ( [System.Management.Automation.PSCredential][parameter(Mandatory = $true)]$Credential ) $networkCredential = New-Object -TypeName System.Net.NetworkCredential -ArgumentList @($Credential.UserName, $Credential.Password) $httpClientHandler = New-Object -TypeName System.Net.Http.HttpClientHandler $httpClientHandler.Credentials = $networkCredential return $httpClientHandler }
Last but not least, a function that actually invokes the post call to our server.
function PostArtifact() { param ( [string][parameter(Mandatory = $true)]$EndpointUrl, [System.Net.Http.HttpClientHandler][parameter(Mandatory = $true)]$Handler, [System.Net.Http.HttpContent][parameter(Mandatory = $true)]$Content ) $httpClient = New-Object -TypeName System.Net.Http.Httpclient $Handler try { $response = $httpClient.PostAsync("$EndpointUrl/service/local/artifact/maven/content", $Content).Result if (!$response.IsSuccessStatusCode) { $responseBody = $response.Content.ReadAsStringAsync().Result $errorMessage = "Status code {0}. Reason {1}. Server reported the following message: {2}." -f $response.StatusCode, $response.ReasonPhrase, $responseBody throw [System.Net.Http.HttpRequestException] $errorMessage } return $response.Content.ReadAsStringAsync().Result } catch [Exception] { $PSCmdlet.ThrowTerminatingError($_) } finally { if($null -ne $httpClient) { $httpClient.Dispose() } if($null -ne $response) { $response.Dispose() } } }
This is all the necessary to upload an artifact to our Nexus server. You can find the just show scripts also on GitHub in the following repository tfs-build-tasks. Further information about programtically uploading artifacts into Nexus can be found in the following post, How can I programatically upload an artifact into Nexus?.
I’ve been trying to use this script, and I can’t seem to figure out why I get this error message whether or not I use the POM file upload or manual GAV.
PostArtifact : You cannot call a method on a null-valued expression.
At C:\nexus1.psm1:50 char:10
+ return PostArtifact $EndpointUrl $httpClientHandler $content
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [PostArtifact], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull,PostArtifact
Kevin, did you ever fix that issue as I am gettjng exact same error and can’t see what is causing it
Similar error, couldn’t find a solution! This method is relevant for uploading artifacts to Sonatype Nexus Repository Manager OS 3.29.2-02?
HELP!!!
PostArtifact : It is not possible to call a method for an expression with a NULL value.
строка:150 знак:16
+ return PostArtifact $EndpointUrl $httpClientHandler $content
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [PostArtifact], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull,PostArtifact
Similar error, couldn’t find a solution! This method is relevant for uploading artifacts to Sonatype Nexus Repository Manager OS 3.29.2-02? HELP!!! Artifact Message: Unable to call a method for an expression with a null value. string: 150 character: 16+ Post Artifact refund $ EndpointUrl $ HttpClientHandler$content + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (:) [PostArtifact], RuntimeException + FullyQualifiedErrorId : InvokeMethodOnNull,PostArtifact