Synchronize Dynamics CRM customizations to TFS Source Control

 

Context

A common problematic encountered in complex development projects is to handle CRM customizations in the same way that we manage other sources (plugin classes, javascript webresources, …).
Source control integration provides the following advantages:

  • Backup : Capability to restore all your CRM customizations
  • Versioning : Capability to get a previous version
  • Comparison : Capability to visualize différences
  • History : Capability to track modifications

3 years ago, I’ve worked with Tanguy Touzard on a batch that call differents applications in order to store a CRM solution to the TFS Source Control.
And, recently, I‘ve decided to rewritte it in order to optimize the process, minimize external applications dependencies and invoke CRM and TFS apis directly from Powershell.

This script use SolutionPackager application to explode CRM customization XML file into structured file and folder tree.

Prerequisites

The script couldn’t run properly without the following criterias:

  1. User who execute the script need to be :
    1. local admin (eventlog creation and write)
    2. contributor on target TFS project
  2. CRM user specified in configuration file need to be system administrator (publih, solution export)
  3. You should provide an Assembliesfolder that contains
    1. CRM SDK assemblies :
      1. Microsoft.Xrm.Sdk.dll
      2. Microsoft.Xrm.Client.dll
      3. Microsoft.Crm.Sdk.Proxy.dll
    2. SolutionPackager.exe
  4. Script must be run with elevated privileges

Process

The Powershell script realizes the following operations:

  1. Eventlog initialization to store process information or errors
  2. Load TFS SDK
  3. Load CRM SDK
  4. Load configuration file
  5. For each synch item
    1. Connect to TFS
    2. Connect to CRM
    3. Publish customization
    4. For each solution
      1. Export solution
      2. Unpack solution into TFS workspace folder
      3. Checkin modifications to TFS

All processing informations or errors are stored into a dedicated log.

Configuration

This script is based on a configuration file that allow to indicate differents CRM organizations, CRM solutions, TFS Project Collections

Sans titre 

 

Each SyncItem represent a synchonization between CRM and TFS.

  • OutputPath : Temporary folder where exported CRM solution is stored
  • CRM > ConnectionString : Connection to CRM organization (More info : https://msdn.microsoft.com/en-us/library/gg695810.aspx)
  • CRM > Solutions > Solution > Name : Unique name of the CRM solution to export
  • TFS > CollectionUrl : Url of the target project collection
  • TFS > WorkspaceFolder : Path to the local TFS folder

 

 

Script

  1: clear;
  2:  
  3: function Add-Crm-Sdk
  4: {
  5:     # Load SDK assemblies
  6:     Add-Type -Path "$PSScriptRoot\Assemblies\Microsoft.Xrm.Sdk.dll";
  7:     Add-Type -Path "$PSScriptRoot\Assemblies\Microsoft.Xrm.Client.dll";
  8:     Add-Type -Path "$PSScriptRoot\Assemblies\Microsoft.Crm.Sdk.Proxy.dll";
  9: }
  10:  
  11: function Add-Tfs-Sdk
  12: {
  13:     # Load TFS SDK assemblies
  14:     [void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation");
  15:     [void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.Common");
  16:     [void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.Client");
  17:     [void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.VersionControl.Client");
  18:     [void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.VersionControl.Common");
  19: }
  20:  
  21: $logName = "MCS.Scripts";
  22: $logSource = "CrmToTfs";
  23:  
  24: function Create-Log()
  25: {
  26:     $logFileExists = Get-EventLog -list | Where-Object {$_.Log -eq $logName } 
  27:     if (!$logFileExists) 
  28:     {
  29:         New-EventLog -LogName $logName -Source $logSource;
  30:     }
  31: }
  32:  
  33: function Log-Error($message)
  34: {
  35:     Write-EventLog -LogName $logName -Source $logSource -EntryType Error -Message $message -EventId 0;
  36: }
  37:  
  38: function Log-Info($message)
  39: {
  40:     Write-EventLog -LogName $logName -Source $logSource -EntryType Information -Message $message -EventId 0;
  41: }
  42:  
  43: function Get-Configuration()
  44: {
  45:     $configFilePath = "$PSScriptRoot\Configuration.xml";
  46:     $content = Get-Content $configFilePath;
  47:     return [xml]$content;
  48: }
  49:  
  50: Create-Log;
  51: Add-Tfs-Sdk;
  52: Add-Crm-Sdk;
  53: $config = Get-Configuration;
  54:  
  55: foreach($syncItem in $config.Configuration.SyncItems.SyncItem)
  56: {
  57:     $d = Get-Date;
  58:     $syncItemName = $syncItem.Name;
  59:     $workspacePath = $syncItem.Tfs.WorkSpaceFolder;
  60:     Write-Host "$d - Crm To Tfs synchronization start for '$syncItemName'" -ForegroundColor Cyan;
  61:  
  62:  
  63:     # =======================================================
  64:     # TFS Connection
  65:     # =======================================================
  66:     $d = Get-Date;
  67:     Write-Host " > $d : Checking TFS connection ..." -NoNewline;
  68:     $tfsUrl = $syncItem.Tfs.CollectionUrl;
  69:     $tfsCollection = [Microsoft.TeamFoundation.Client.TfsTeamProjectCollectionFactory]::GetTeamProjectCollection($tfsUrl);
  70:     $tfsCollection.EnsureAuthenticated();
  71:     if($tfsCollection.HasAuthenticated)
  72:     {
  73:         Write-Host "Authenticated to TFS!" -ForegroundColor Green;
  74:         Log-Info "Checking TFS connection ... Authenticated to TFS!";
  75:     }
  76:     else
  77:     {
  78:         Write-Host "Not authenticated to TFS ..." -ForegroundColor Red;
  79:         Log-Error "Checking TFS connection ... Not authenticated to TFS";
  80:         return;
  81:     }
  82:  
  83:     # =======================================================
  84:     # Crm Connection
  85:     # =======================================================
  86:     $crmConnectionString = $syncItem.Crm.ConnectionString;
  87:     $crmConnection = [Microsoft.Xrm.Client.CrmConnection]::Parse($crmConnectionString);
  88:     $service = New-Object -TypeName Microsoft.Xrm.Client.Services.OrganizationService -ArgumentList $crmConnection;
  89:  
  90:     # =======================================================
  91:     # Publish
  92:     # =======================================================
  93:     $d = Get-Date;
  94:     Write-Host " > $d : Publishing customizations ..." -NoNewline;
  95:     $publishRequest = New-Object -TypeName Microsoft.Crm.Sdk.Messages.PublishAllXmlRequest;
  96:     try
  97:     {
  98:         $publishResponse = $service.Execute($publishRequest);
  99:         Write-Host "done!" -ForegroundColor Green;
  100:         Log-Info "Publishing customizations ... done!";
  101:     }
  102:     catch [Exception]
  103:     {
  104:         Write-Host "failed! [Error : $_.Exception]" -ForegroundColor Red;
  105:         Log-Error "Publishing customizations ... failed![Error : $_.Exception]";
  106:         return;
  107:     }  
  108:  
  109:     
  110:     $outputPath = $syncItem.OutputPath;
  111:     $syncItemPath = [System.IO.Path]::Combine($outputPath, $syncItem.Name);
  112:    
  113:     New-Item -ErrorAction Ignore -ItemType directory -Path $syncItemPath;
  114:  
  115:     foreach($solution in $syncItem.Crm.Solutions.Solution)
  116:     {
  117:         # =======================================================
  118:         # Export solution
  119:         # =======================================================
  120:     
  121:         $solutionToExport = $solution.Name;
  122:         $solutionPath = [System.IO.Path]::Combine($syncItemPath, "$solutionToExport.zip");
  123:  
  124:         $d = Get-Date;
  125:         Write-Host " > $d : Exporting solution ($solutionToExport) to path '$solutionPath' ... " -NoNewline;
  126:         $request = New-Object -TypeName Microsoft.Crm.Sdk.Messages.ExportSolutionRequest;
  127:         $request.SolutionName = $solutionToExport;
  128:         $request.Managed = $false;    
  129:         $request.ExportCalendarSettings = $true;
  130:         $request.ExportCustomizationSettings = $true;
  131:         $request.ExportEmailTrackingSettings = $true;
  132:         $request.ExportAutoNumberingSettings = $true;
  133:         $request.ExportIsvConfig = $true;
  134:         $request.ExportOutlookSynchronizationSettings = $true;
  135:         $request.ExportGeneralSettings = $true;
  136:         $request.ExportMarketingSettings = $true;
  137:         $request.ExportRelationshipRoles = $true;
  138:         try
  139:         {
  140:             $response = $service.Execute($request);
  141:             [System.IO.File]::WriteAllBytes($solutionPath, $response.ExportSolutionFile);
  142:             Write-Host "done!" -ForegroundColor Green;
  143:             Log-Info "Exporting solution ... done!";
  144:         }
  145:         catch [Exception]
  146:         {
  147:             Write-Host "failed! [Error : $_.Exception]" -ForegroundColor Red;
  148:             Log-Error "Exporting solution ... failed! [Error : $_.Exception]";
  149:             return;
  150:         }            
  151:  
  152:         # =======================================================
  153:         # Unpack solution
  154:         # =======================================================
  155:         $d = Get-Date;
  156:                 
  157:         $syncItemWorkspacePath = [System.IO.Path]::Combine($workspacePath, $syncItem.Name);
  158:         $solutionWorkspacePath = [System.IO.Path]::Combine($syncItemWorkspacePath, $solution.Name);
  159:         
  160:         $logPath = $solutionPath.Replace(".zip", "-SolutionPackager.log");
  161:         Write-Host " > $d : Unpacking solution to path '$solutionWorkspacePath' ... " -NoNewline;
  162:  
  163:         $processArgs = [String]::Concat('/action:Extract /zipfile:"', $solutionPath, '" /folder:"', $solutionWorkspacePath, '" /clobber /errorlevel:Verbose /log:"', $logPath, '" /allowDelete:Yes /allowWrite:Yes');
  164:         $process = "$PSScriptRoot\Assemblies\SolutionPackager.exe";
  165:         Start-Process -FilePath  $process -ArgumentList $processArgs -Wait -Verbose -Debug;
  166:         Write-Host "done!" -ForegroundColor Green;
  167:         Log-Info "Unpacking solution ... done!";
  168:     }
  169:  
  170:     # =======================================================
  171:     # Checkin modifications to TFS Source Control
  172:     # =======================================================
  173:     $d = Get-Date;
  174:     Write-Host " > $d : Checking pending changes to TFS ... " -NoNewline;
  175:     $workspaceInfo = [Microsoft.TeamFoundation.VersionControl.Client.Workstation]::Current.GetLocalWorkspaceInfo($workspacePath);
  176:     $workspace = $workspaceInfo.GetWorkspace($tfsCollection);    
  177:     $workspace.PendAdd($workspacePath, $true);
  178:  
  179:     $pendingChanges = $workspace.GetPendingChanges() | Where-Object {$_.LocalItem.StartsWith($solutionWorkspacePath) };
  180:     try
  181:     {
  182:         $pendingChangesCount = $pendingChanges.Count;
  183:  
  184:         # Ignore solution.xml if it's the only modifications
  185:         $processCheckin = $true;
  186:         if($pendingChangesCount -eq 0)
  187:         {
  188:             $processCheckin = $false;
  189:             Write-Host "ignored! (nothing to checkin)" -ForegroundColor Green;
  190:             Log-Info "Checking-in modifications to TFS... ignored (nothing to checkin)!";
  191:         }
  192:         elseif($pendingChangesCount -eq 1)
  193:         {
  194:             $pendingChange = $pendingChanges | Select-Object -First 1;
  195:             if($pendingChange.LocalItem.Contains("Solution.xml"))
  196:             {
  197:                 $processCheckin = $false;
  198:                 Write-Host "ignored! (solution.xml)" -ForegroundColor Green;
  199:                 Log-Info "Checking-in modifications to TFS... ignored (solution.xml)!";
  200:                 $workspace.Undo($pendingChanges);
  201:             }
  202:         }
  203:  
  204:         if($processCheckin)
  205:         {
  206:             $result = $workspace.CheckIn($pendingChanges, "Crm To Tfs Synchronization : $pendingChangesCount item(s)");
  207:             Write-Host "done! (ChangeSet : $result)" -ForegroundColor Green;
  208:             Log-Info "Checking-in modifications to TFS... done!";
  209:         }
  210:     }
  211:     catch [Exception]
  212:     {
  213:         Write-Host "failed! [Error : $_.Exception]" -ForegroundColor Red;
  214:         Log-Error "Checking-in modifications to TFS... failed! [Error : $_.Exception]";
  215:         return;
  216:     }
  217:  
  218:     $d = Get-Date;
  219:     Write-Host "$d - Crm To Tfs synchronization stop for '$syncItemName'" -ForegroundColor Cyan;
  220:  
  221: }

Download the full version

 

Usage

The script could be run manually or triggered by a windows scheduled task (More info : https://blogs.technet.com/b/heyscriptingguy/archive/2012/08/11/weekend-scripter-use-the-windows-task-scheduler-to-run-a-windows-powershell-script.aspx)

You could also run it with Azure Automation or a TFS build, but you need to host the TFS and CRM SDK assemblies in a centralized storage.

 

Have fun!