[D365] Create Azure feed from build pipeline
![[D365] Create Azure feed from build pipeline](/content/images/size/w2000/2019/10/azureFeedsCreationDialog-1.png)
Using the pipelines in Azure DevOps to build D365fO packages is a great way to automate builds and share the resulting artifacts.
A great way to provide the generated (deployable) packages to other systems is using Azure Artifact Feeds.
Creating a feed in DevOps
Switch to the DevOps project you want to create a feed in and select the Artifacts tile on the left side menu:

Create a new feed via the button at the top panel:


Enter a name and set the options who can see this feed.
A rule of thumb is to set it either to your Azure AD or invite specific people.
Connect a feed
This section is only informational, we will setup the push of packages to this feed within a pipeline task.
To connect to this feed, click the Connect to feed button and see the options in the following dialog:


Setup pipeline to push artifacts to feed
Now we want to have our pipeline to push the artifacts from the build (deployable package in our case) to this feed so we don't have to do this manually every time we successfully ran a build.
You'll have tasks similar to this setup in your pipeline, where Generate Packages and Publish Artifacts: Packages are the ones of interest for us:

Task: create nuspec file
Add a new task of type PowerShell and set it up to run a script to generate a NuSpec file:

The script is as following:
# The path of the artifact to extract.
$artifactPath = $Env:AGENT_BUILDDIRECTORY
$stagingDirectory = $Env::BUILD_ARTIFACTSTAGINGDIRECTORY
$sourceType = "SoftwareDeployablePackage"
$binariesPath = "$stagingDirectory\Files"
$metadataPath = $binariesPath
$iconUrl = ""
<# param ( [parameter(mandatory="$false," helpmessage="The path of the artifact to extract." )] [string]$artifactpath, [string]$stagingdirectory, [string]$sourcetype, [string]$binariespath="$($stagingDirectory)\Files" , [string]$metadatapath="$binariesPath," [string]$iconurl ) #>
function Get-NuSpecXML() {
# xmlns=`"http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd`"
$xml = New-Object xml
$xml.PreserveWhitespace = $True
$xml.LoadXml("
ApplicationPlatform
7.0.4030.2
Microsoft
Microsoft
Application Platform
Copyright 2016 Microsoft
")
$xml
}
function Add-File([xml]$xml, [string]$source, [string]$target) {
$fileNode = $xml.CreateElement("file")
$attribute = $xml.CreateAttribute("src")
$attribute.Value = $source
$attribute = $fileNode.Attributes.Append($attribute)
$attribute = $xml.CreateAttribute("target")
$attribute.Value = $target
$attribute = $fileNode.Attributes.Append($attribute)
$fileNode = $xml.package.files.AppendChild($fileNode)
}
function Create-NuSpec([string]$PackagePath) {
$PackageName = (Get-Item $PackagePath).BaseName
$NugetPackageName = $PackageName
$PackageBinPath = Join-Path $PackagePath "bin"
$xml = Get-NuSpecXML
$xml.package.metadata.id = "$NugetPackageName"
$xml.package.metadata.title = $xml.package.metadata.id
$xml.package.metadata.description = "Dynamics 365 for Finance and Operations package - $NugetPackageName"
$owner = ""
[xml]$descriptorXML = "";
# Try to find main descriptor by looking for the package name as the model name
$descriptorPath = Join-Path $PackagePath "Descriptor"
#$descriptorPath = $descriptorPath.ToLower().Replace($filesPath.ToLower(), $sourcesPath)
$packageMainDescriptor = Join-Path $descriptorPath "$PackageName.xml"
if (!(Test-Path $packageMainDescriptor) -and (Test-Path $descriptorPath)) {
# Get all descriptors
$packageDescriptor = Get-ChildItem $descriptorPath -Filter *.xml | Where-Object { $_.BaseName.Replace(" ", "") -eq $PackageName }
if ($packageDescriptor.Count -gt 0) {
$packageMainDescriptor = Join-Path $descriptorPath "$($packageDescriptor[0].BaseName).xml"
}
else {
$packageMainDescriptor = Join-Path $descriptorPath "$($packageDescriptor.BaseName).xml"
}
}
if (Test-Path $packageMainDescriptor) {
[xml]$descriptorXML = Get-Content $packageMainDescriptor -Encoding UTF8
if ($descriptorXML.AxModelInfo.Publisher) {
$owner = $descriptorXML.AxModelInfo.Publisher
}
$xml.package.metadata.copyright = "Copyright $((Get-Date).Year) $owner"
$xml.package.metadata.authors = $owner
$xml.package.metadata.owners = $owner
$xml.package.metadata.title = $descriptorXML.AxModelInfo.DisplayName
if ([string]::IsNullOrEmpty($descriptorXML.AxModelInfo.Description)) {
$xml.package.metadata.description = "Dynamics 365 for Finance and Operations package - $NugetPackageName"
}
}
$PackageDLL = Join-Path $PackageBinPath "Dynamics.AX.$PackageName.dll"
if (Test-Path $PackageDLL) {
$PackageVersion = (Get-Item $PackageDLL).VersionInfo.FileVersion
$xml.package.metadata.version = $PackageVersion
}
else {
$xml.package.metadata.version = "$($descriptorXML.AxModelInfo.VersionMajor).$($descriptorXML.AxModelInfo.VersionMinor).$($descriptorXML.AxModelInfo.VersionBuild).$($descriptorXML.AxModelInfo.VersionRevision)"
}
switch -Regex ($sourceType) {
"Binaries|SoftwareDeployablePackage" {
Add-File -xml $xml -source ".\bin\**" -target ".\bin\"
if (Test-Path $(Join-Path $PackagePath "Reports")) {
Add-File -xml $xml -source ".\Reports\**" -target ".\Reports\"
}
if (Test-Path $(Join-Path $PackagePath "Resources")) {
Add-File -xml $xml -source ".\Resources\**" -target ".\Resources\"
}
if (Test-Path $(Join-Path $PackagePath "WebContent")) {
Add-File -xml $xml -source ".\WebContent\**" -target ".\WebContent\"
}
if (Test-Path $(Join-Path $PackagePath "FileLocations.xml")) {
Add-File -xml $xml -source ".\FileLocations.xml" -target ".\"
}
#Add-File -xml $xml -source ".\Resources\**" -target ".\Resources\"
#Add-File -xml $xml -source ".\WebContent\**" -target ".\WebContent\"
#Add-File -xml $xml -source ".\FileLocations.xml" -target ".\"
}
"Model|SourceCode" {
#$PackagePath = Join-Path $sourceDirectory -ChildPath $PackageName
if (Test-Path $(Join-Path $PackagePath "Descriptor")) {
Add-File -xml $xml -source ".\Descriptor\**" -target ".\Descriptor\"
#Add-File -xml $xml -source "$(Join-Path $PackagePath "Descriptor")\**" -target ".\Descriptor\"
$descriptorPath = Join-Path $PackagePath "Descriptor"
$models = (Get-ChildItem $descriptorPath -Filter *.xml) | ForEach-Object { $_.BaseName }
foreach ($model in $models) {
Add-File -xml $xml -source ".\$model\**" -target ".\$model\"
# Add-File -xml $xml -source "$(Join-Path $PackagePath $model)\**" -target ".\$model\"
}
}
}
}
$nuSpecFile = Join-Path $PackagePath "$NugetPackageName.nuspec"
$xml.Save($nuSpecFile)
Write-Host "NuSpec for $PackagePath created."
}
#$PSScriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
#$solutionPath = "$PSScriptRoot"
$filesPath = Join-Path $stagingDirectory -ChildPath "Files"
New-Item -ItemType Directory -Force -Path $filesPath
switch -regex ($sourceType) {
"SoftwareDeployablePackage" {
$extractPath = Join-Path $stagingDirectory -ChildPath "Extracted"
# $filesPath = Join-Path $stagingDirectory -ChildPath "Files"
Write-Host $extractPath
#Remove-Item $filesPath -Recurse -ErrorAction Ignore
Get-ChildItem -Path $artifactPath -Include AXDeployable*.zip -Recurse | % {
Write-Host $_.Name
Remove-Item $extractPath -Recurse -ErrorAction Ignore
Expand-Archive -LiteralPath $_.FullName -DestinationPath $extractPath
$nupkgPath = Join-Path $extractPath -ChildPath "AOSService\Packages"
Get-ChildItem $nupkgPath -Filter *.nupkg |
Foreach-Object {
Write-Host $_.Name
#Write-Host $($_.BaseName).Replace("dynamicsax-","")
Rename-Item $_.FullName -NewName $($_.Name).Replace(".nupkg", ".zip");
$nupkgExtractPath = Join-Path $extractPath -ChildPath "nupkg"
Remove-Item $nupkgExtractPath -Recurse -ErrorAction Ignore
Expand-Archive -LiteralPath $_.FullName.Replace(".nupkg", ".zip") -DestinationPath $nupkgExtractPath
$nuspecPath = Get-ChildItem $nupkgExtractPath -Filter *.nuspec | Select-Object -First 1
$nuspecContent = [XML](Get-Content $nuspecPath.FullName)
Write-Host $nuspecContent.package.metadata.summary
$fileZipPath = Join-Path "$($nupkgPath)\files" -ChildPath $($_.Name).Replace(".nupkg", ".zip")
$packageExtractPath = Join-Path $filesPath -ChildPath $nuspecContent.package.metadata.summary
#Remove-Item $packageExtractPath -Recurse -ErrorAction Ignore
Expand-Archive -LiteralPath $fileZipPath -DestinationPath $packageExtractPath -Force
}
}
}
"Model" {
$extractPath = Join-Path $stagingDirectory -ChildPath "Extracted"
# $filesPath = Join-Path $stagingDirectory -ChildPath "Files"
Write-Host $extractPath
#Remove-Item $filesPath -Recurse -ErrorAction Ignore
Get-ChildItem -Path $artifactPath -Include AXModelSource*.zip -Recurse | % {
Write-Host $_.Name
Remove-Item $extractPath -Recurse -ErrorAction Ignore
Expand-Archive -LiteralPath $_.FullName -DestinationPath $extractPath
Get-ChildItem $extractPath -Filter *.axmodel |
Foreach-Object {
Write-Host $_.Name
#Write-Host $($_.BaseName).Replace("dynamicsax-","")
Rename-Item $_.FullName -NewName $($_.Name).Replace(".axmodel", ".zip");
$modelExtractPath = Join-Path $extractPath -ChildPath $_.Name
Remove-Item $modelExtractPath -Recurse -ErrorAction Ignore
Expand-Archive -LiteralPath $_.FullName.Replace(".axmodel", ".zip") -DestinationPath $modelExtractPath
$packages = Join-Path $modelExtractPath -ChildPath "Static"
Get-ChildItem -Directory $packages | % {
Write-Host $_.Name
Copy-Item -Container $_.FullName -Destination $filesPath -Recurse -Force
}
}
}
}
"Binaries|SourceCode" {
if ($metadataPath -ne $filesPath) {
Get-ChildItem -Directory $metadataPath | % {
Copy-Item -Path $_.FullName -Destination $(Join-Path $filesPath -ChildPath $_.Name) -Recurse -Force
}
}
if ($binariesPath -ne $filesPath) {
Get-ChildItem -Directory $binariesPath | % {
Copy-Item -Container $_.FullName -Destination $filesPath -Recurse -Force
}
}
}
}
if (![String]::IsNullOrEmpty($filesPath) -and $(Test-Path -Path $filesPath)) {
Get-ChildItem -Directory $filesPath | % { Create-NuSpec $_.FullName }
Get-ChildItem -Path $filesPath -Include *.nuspec -Recurse | % { Write-Host $_.Name }
}
Task: pack nuget
Add another task right below. Search for nuget pack
and add the NuGet task after the publish task:

Configure it to pack the deployable package with the NuSpec file from the previous step:

You can find a Reference for this task in the Docs.
Task: push nuget
Add another NuGet task right below for the push
command:

Enter the feed URL from the Connect to feed dialog before as target feed.
You can use the dropdown to select feeds within the same organization.
Run the pipeline
Run the pipeline and wait until it's done.
Review the logs of the tasks, especially the newly added NuGet tasks.
Check your feed
Your feed should now contain a NuGet package with your artifacts from the pipeline (the deployable package) and can be consumed by others.

Consume a D365 deployable package via feed
In the release or pipeline where you want to add a package from a feed to the resulting deployable package, create a new NuGet task and configure it as
Command: custom
Command and arguments: install MyModule -excludeversion -source https://$(NugetAccount).pkgs.visualstudio.com/_packaging/COMPANY.PROJECT.Packages/nuget/v3/index.json -OutputDirectory "$(NugetOutputDirectory)"
Replace MyModule with your package name and COMPANY.PROJECT.Packages with the name of your feed you want to consume.

The two variables are configured as followed:
NugetAccount: Name of your DevOps instance
NugetOutputDirectory: $(Build.SourcesDirectory)\Metadata
