[D365] Azure Dev Ops build with git as source control

In a previous article I shared the transition from TFVC to GIT as source control provider with Dynamics 365 for Operations (D356fO).

Microsoft does not support Azure Dev Ops (VSTS) automated builds for git repositories by default. When deploying a build environment from LCS, it creates a template build definition with all necessarry steps in the connected VSTS Project - but only for TFVC repositories!

One of the build steps is the one for MSBuild to build the models:

This uses the file AXModulesBuild.proj, which our build environment generated for us with the help of the DynamicsSDK PowerShell Scripts.

If we just change the repository to our git repository and run the build, it fails telling us it could not find the Metadata path!

2018-12-04T11:05:01.9568535Z   VERBOSE: 11:05:01 AM: - Event: EventWriteDevALMTaskGenerateProjFilesStart
2018-12-04T11:05:01.9678157Z   11:05:01 AM: Script completed with exit code: 0
2018-12-04T11:05:01.9694698Z   VERBOSE: Importing function 'Get-AX7SdkDeploymentBinariesPath'.
2018-12-04T11:05:01.9702542Z   VERBOSE: Importing function 'Get-AX7SdkDeploymentMetadataPath'.
2018-12-04T11:05:01.9710158Z   VERBOSE: Importing function 'Set-AX7SdkRegistryValues'.
2018-12-04T11:05:01.9717248Z   VERBOSE: Importing function 'Write-Message'.
2018-12-04T11:05:01.9731443Z   11:05:01 AM: Generating build projects for models in: C:\DynamicsSDK\VSOAgent\_work\3\s\Metadata
2018-12-04T11:05:01.9990008Z   VERBOSE: 11:05:01 AM: - Exception thrown at 
2018-12-04T11:05:01.9990637Z   C:\DynamicsSDK\GenerateProjFiles.ps1:470: throw "Specified metadata path does 
2018-12-04T11:05:01.9990949Z   not exist: $MetadataPath"

What is the Metadata path anyway?

It is the path ..\AOSService\PackagesLocalDirectory!
In a TFVC repository you'll find a folder called Metadata which you map to you PackagesLocalDirectory folder.

Why does it not exist in my git repository?

Simply, because git maps the whole repository at once and not single folders individually.
So the ..\AOSService\PackagesLocalDirectory path is already our Metadata folder!

How do I get the build to use the correct directory?

Edit your AXModulesBuild.proj .
Remove the trailing \Metadata from the commands for Generating Modules.proj.
The key starts with <Target Name="GenerateProjFiles"

Original:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="14.0">
  <PropertyGroup>
    <MetadataDirectory>C:\AOSService\PackagesLocalDirectory</MetadataDirectory>
    <DynamicsSDK>$(registry:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Dynamics\AX\7.0\SDK@DynamicsSDK)</DynamicsSDK>
    <PackagesPath>$(AGENT_BUILDDIRECTORY)\Packages</PackagesPath>
    <TestResultsPath>$(AGENT_BUILDDIRECTORY)\TestResults</TestResultsPath>
    <ArtifactsPath>$(AGENT_BUILDDIRECTORY)\Artifacts</ArtifactsPath>
  </PropertyGroup>

  <!-- Instrument that build has started -->
  <Target Name="InstrumentBuildStart" Condition="Exists('$(DynamicsSDK)\DevALMInstrumentor.ps1')">
    <Exec Command="powershell.exe -NonInteractive -ExecutionPolicy RemoteSigned -Command &quot;&amp; {$(DynamicsSDK)\DevALMInstrumentor.ps1 -TaskBuildStart}&quot;" ContinueOnError="WarnAndContinue" />
  </Target>

  <!-- Clean output from previous build -->
  <Target Name="Clean" AfterTargets="InstrumentBuildStart">
    <RemoveDir Directories="$(OutputPath)" Condition="Exists('$(OutputPath)')" />
    <RemoveDir Directories="$(LogPath)" Condition="Exists('$(LogPath)')" />
    <RemoveDir Directories="$(PackagesPath)" Condition="Exists('$(PackagesPath)')" />
    <RemoveDir Directories="$(TestResultsPath)" Condition="Exists('$(TestResultsPath)')" />
    <RemoveDir Directories="$(ArtifactsPath)" Condition="Exists('$(ArtifactsPath)')" />
  </Target>

  <!-- Generate Metadata.proj -->
  <Target Name="GenerateProjFiles" AfterTargets="InstrumentBuildStart" Condition="Exists('$(DynamicsSDK)\GenerateProjFiles.ps1')">
    <Exec Command="powershell.exe -NonInteractive -ExecutionPolicy RemoteSigned -Command &quot;&amp; {$(DynamicsSDK)\GenerateProjFiles.ps1 -MetadataPath '$(MSBuildProjectDirectory)\Metadata' -ProjectMetadataDependencyXml '$(DependencyXml)' -ErrorLogPath '$(LogPath)\GenerateProjErrors.log' -Verbose}&quot;" ContinueOnError="ErrorAndStop" />
    <Error Condition="Exists('$(LogPath)\GenerateProjErrors.log')" Text="Failed to generate project files. Please check GenerateProjErrors.log in the additional logs for details." />
  </Target>

  <!-- Build generated projects -->
  <Target Name="ExecuteMetadata_Project_BuildProj" AfterTargets="GenerateProjFiles" Condition="Exists('Metadata_Project_Build.proj')">
    <MSBuild Projects="Metadata_Project_Build.proj" ContinueOnError="ErrorAndStop" StopOnFirstFailure="true"/>
  </Target>

  <!-- Ensure that the output path exists as it is passed to the Generate Packages and Execute Tests steps -->
  <Target Name="CreateOutputPath" AfterTargets="ExecuteMetadata_Project_BuildProj">
    <MakeDir Directories="$(OutputPath)" Condition="!Exists('$(OutputPath)')" />
  </Target>

  <!-- Instrument that build has ended -->
  <Target Name="InstrumentBuildEnd" AfterTargets="CreateOutputPath" Condition="Exists('$(DynamicsSDK)\DevALMInstrumentor.ps1')">
    <Exec Command="powershell.exe -NonInteractive -ExecutionPolicy RemoteSigned -Command &quot;&amp; {$(DynamicsSDK)\DevALMInstrumentor.ps1 -TaskBuildEnd}&quot;" ContinueOnError="WarnAndContinue" />
  </Target>
</Project>

New file:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="14.0">
  <PropertyGroup>
    <MetadataDirectory>C:\AOSService\PackagesLocalDirectory</MetadataDirectory>
    <DynamicsSDK>$(registry:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Dynamics\AX\7.0\SDK@DynamicsSDK)</DynamicsSDK>
    <PackagesPath>$(AGENT_BUILDDIRECTORY)\Packages</PackagesPath>
    <TestResultsPath>$(AGENT_BUILDDIRECTORY)\TestResults</TestResultsPath>
    <ArtifactsPath>$(AGENT_BUILDDIRECTORY)\Artifacts</ArtifactsPath>
  </PropertyGroup>

  <!-- Instrument that build has started -->
  <Target Name="InstrumentBuildStart" Condition="Exists('$(DynamicsSDK)\DevALMInstrumentor.ps1')">
    <Exec Command="powershell.exe -NonInteractive -ExecutionPolicy RemoteSigned -Command &quot;&amp; {$(DynamicsSDK)\DevALMInstrumentor.ps1 -TaskBuildStart}&quot;" ContinueOnError="WarnAndContinue" />
  </Target>

  <!-- Clean output from previous build -->
  <Target Name="Clean" AfterTargets="InstrumentBuildStart">
    <RemoveDir Directories="$(OutputPath)" Condition="Exists('$(OutputPath)')" />
    <RemoveDir Directories="$(LogPath)" Condition="Exists('$(LogPath)')" />
    <RemoveDir Directories="$(PackagesPath)" Condition="Exists('$(PackagesPath)')" />
    <RemoveDir Directories="$(TestResultsPath)" Condition="Exists('$(TestResultsPath)')" />
    <RemoveDir Directories="$(ArtifactsPath)" Condition="Exists('$(ArtifactsPath)')" />
  </Target>

  <!-- Generate Metadata.proj -->
  <Target Name="GenerateProjFiles" AfterTargets="InstrumentBuildStart" Condition="Exists('$(DynamicsSDK)\GenerateProjFiles.ps1')">
    <Exec Command="powershell.exe -NonInteractive -ExecutionPolicy RemoteSigned -Command &quot;&amp; {$(DynamicsSDK)\GenerateProjFiles.ps1 -MetadataPath '$(MSBuildProjectDirectory)' -ProjectMetadataDependencyXml '$(DependencyXml)' -ErrorLogPath '$(LogPath)\GenerateProjErrors.log' -Verbose}&quot;" ContinueOnError="ErrorAndStop" />
    <Error Condition="Exists('$(LogPath)\GenerateProjErrors.log')" Text="Failed to generate project files. Please check GenerateProjErrors.log in the additional logs for details." />
  </Target>

  <!-- Build generated projects -->
  <Target Name="ExecuteMetadata_Project_BuildProj" AfterTargets="GenerateProjFiles" Condition="Exists('Metadata_Project_Build.proj')">
    <MSBuild Projects="Metadata_Project_Build.proj" ContinueOnError="ErrorAndStop" StopOnFirstFailure="true"/>
  </Target>

  <!-- Ensure that the output path exists as it is passed to the Generate Packages and Execute Tests steps -->
  <Target Name="CreateOutputPath" AfterTargets="ExecuteMetadata_Project_BuildProj">
    <MakeDir Directories="$(OutputPath)" Condition="!Exists('$(OutputPath)')" />
  </Target>

  <!-- Instrument that build has ended -->
  <Target Name="InstrumentBuildEnd" AfterTargets="CreateOutputPath" Condition="Exists('$(DynamicsSDK)\DevALMInstrumentor.ps1')">
    <Exec Command="powershell.exe -NonInteractive -ExecutionPolicy RemoteSigned -Command &quot;&amp; {$(DynamicsSDK)\DevALMInstrumentor.ps1 -TaskBuildEnd}&quot;" ContinueOnError="WarnAndContinue" />
  </Target>
</Project>

Check it in and run the build!

We had some cases where we also needed to customize the entry under Project.PropertyGroup.MetadataDirectory with a hardcoded string for the respective build environment topology or to set it in the registry on the build maschine with this PowerShell script:

$registryPath = "HKLM:\SOFTWARE\Microsoft\Dynamics\AX\7.0\SDK"
$metadataKeyName = "MetadataPath"
$metadataKeyValue = "K:\AosService\PackagesLocalDirectory"
$deployBinariesKeyName = "BinariesPath"
$deployBinariesKeyValue = "K:\AosService\PackagesLocalDirectory\Bin"
$packagesPathKeyName = "PackagesPath"
$packagesPathKeyValue = "K:\AosService\PackagesLocalDirectory"
# create entry for MetadataPath
if (!(Test-Path $registryPath)) {
    New-Item -Path $registryPath -Force | Out-Null
    New-ItemProperty -Path $registryPath -Name $metadataKeyName -Value $metadataKeyValue -PropertyType "String" -Force | Out-Null
}
else {
    New-ItemProperty -Path $registryPath -Name $metadataKeyName -Value $metadataKeyValue -PropertyType "String" -Force | Out-Null
}
# create entry for BinariesPath
if (!(Test-Path $registryPath)) {
    New-Item -Path $registryPath -Force | Out-Null
    New-ItemProperty -Path $registryPath -Name $deployBinariesKeyName -Value $deployBinariesKeyValue -PropertyType "String" -Force | Out-Null
}
else {
    New-ItemProperty -Path $registryPath -Name $deployBinariesKeyName -Value $deployBinariesKeyValue -PropertyType "String" -Force | Out-Null
}
# create entry for PackagesPath
if (!(Test-Path $registryPath)) {
    New-Item -Path $registryPath -Force | Out-Null
    New-ItemProperty -Path $registryPath -Name $packagesPathKeyName -Value $packagesPathKeyValue -PropertyType "String" -Force | Out-Null
}
else {
    New-ItemProperty -Path $registryPath -Name $packagesPathKeyName -Value $packagesPathKeyValue -PropertyType "String" -Force | Out-Null
}
pause

As last step, customize the file GenerateProjFiles.ps1:

$Metadata_Project_Build_Path = (Get-Item -Path $MetadataPath).Parent.FullName

New:
$Metadata_Project_Build_Path = (Get-Item -Path $MetadataPath)