Exploration: Implement CI/CD in Dataverse

In this blog post, I will share a bit of my experience in implementing CI/CD on the Dataverse Environment. Even now is not perfect and still got lots of room to improve, I hope you guys can learn something from it.

Creating The Exe

I currently not using any features on official tools (Power Platform Build Tools) because it needs us to configure which solutions that we want to export. So that’s why I create my own tools that I store in the repository.

Here is the sample of the code that I created (the Program.cs):

using CommandLine;
using CrmDeployment.Business;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Tooling.Connector;
using System;

namespace CrmDeployment
{
    class Program
    {
        static void Main(string[] args)
        {
            Parser.Default.ParseArguments<CommandLineOptions>(args)
                .WithParsed(option => Run(option));
            Console.Read();
        }

        static void Run(CommandLineOptions option)
        {
            var client = new CrmServiceClient(option.ConnectionString);

            Console.WriteLine($"Connected to {client.ConnectedOrgFriendlyName}");

            switch (option.Type)
            {
                case "Update-WebResource":
                    {
                        new UpsertWebResources(client, option.Directory).Execute();

                        var publishAll = new PublishAllXmlRequest();
                        client.Execute(publishAll);

                        Console.WriteLine("Publish All finished");
                        break;
                    }

                case "Update-Plugin":
                    new UpdatePlugin(client, option.FilePath).Execute();
                    break;
                case "Update-Plugins":
                    new UpdatePlugins(client, option.Directory).Execute();
                    break;
                case "Export-Solutions":
                    {
                        var publishAll = new PublishAllXmlRequest();
                        client.Execute(publishAll);

                        Console.WriteLine("Publish All finished");

                        new SolutionsExporter(client, option.Directory, option.IsManaged.GetValueOrDefault()).Execute();
                        break;
                    }
                case "Import-Solutions":
                    {
                        new SolutionsImporter(client, option.Directory, option.IsToday.GetValueOrDefault()).Execute();
                        break;
                    }
            }
        }
    }
}

On the above code, you can see that I’m using the CommandLineParser NuGet package that makes my code super tidy! You just need to define the model that you will use and define all the properties that will help users to understand more (CommandLineOptions.cs):

using CommandLine;

namespace CrmDeployment
{
    public class CommandLineOptions
    {
        [Value(index: 0, Required = true, HelpText = "CRM Connection string.")]
        public string ConnectionString { get; set; }
        [Value(index: 1, Required = true, HelpText = "Type of the execution console.")]
        public string Type { get; set; }
        [Option(shortName: 'f', longName: "filepath", Required = false, HelpText = "FilePath of plugin.")]
        public string FilePath { get; set; }
        [Option(shortName: 'd', longName: "directory", Required = false, HelpText = "Directory")]
        public string Directory { get; set; }
        [Option(shortName: 'm', longName: "managed", Required = false, HelpText = "Managed")]
        public bool? IsManaged { get; set; }
        [Option(shortName: 't', longName: "today", Required = false, HelpText = "Import Today Solution")]
        public bool? IsToday { get; set; }
    }
}

In the Run method, you can see all the business logic that will be called based on the Type that we pass on the exe. The connection string will be passed on to the exe arguments. So we only need to configure when we call the exe later on. All the source code you can access on this GitHub repo.

Creating App Registration in Azure AD+Application User

Because we don’t want to use user authentication (bypass the MFA), creating App Registration is the easiest + better solution. 

You can go to portal.azure.com > Azure Active Directory > App registrations > New Registration

You just need to give a Name, and you can click the Register button:

Create App Registration

After that you can go to API Permissions blade > Add a permission > select Dynamics CRM > check the user_impersonation > click Add permissions button. 

Add Dynamics CRM permission to the App Registration

Then you can go to the Certificates & secrets blade > Client secrets New client secret:

Create Secret

Enter the description and click the Add button. Once you do that, you can copy the Secret Value (it will be gone after that, so you need to save it somewhere) and you also need to go Overview blade to get the Application (client) ID.

Once this is done, we need to register our Application Id in CRM. Go to your CRM environment Settings Security Users > Change the view to Application Users > click New button (if the Form is not selected to Application User form, you can change manually). You just need to copy-paste the Application ID and click save. Once done, you need to assign a System administrator role to this user.

Create CRM Application User

The only left is in this section is to create the connection string that will be consumed in the exe. You need to use this connection string format:

AuthType=ClientSecret;Url=<your_crm_url>;ClientId=<your_application_id>;ClientSecret=<client_secret_value>

Create Azure Pipeline

The source of the repository in this demonstration is in Azure Repos Git (not GitHub). So the steps that will be run for this sample will be seen as follow:

CI/CD Flow in this post

Like I said before, this flow is not yet perfect. But at least, I want to encourage you to try it and give your comment on it. This is the screenshot of my root repo folder:

My repo here

CRMDeployment folder contains the exe that has been built in the first section (Creating The Exe). While in the src for currently only will contain Plugins folder (I already got samples of plugin code + unit tests there).

To create the Azure Pipeline (I will not show you how to set up the whole repo, just directly explain the Azure Pipeline only). To create the Azure Pipeline, go to dev.azure.com > Pipelines blade > New pipeline button > select Azure Repos Git > select your repository > select starter pipeline and change it as follows (YAML file):

trigger:
  branches:
    include: 
       - master
  paths:
    exclude:
      - '**/*.zip'
      - CrmSolutions

pool:
  vmImage: 'windows-latest'
  environment: 'Dev'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
  
- checkout: self
  persistCredentials: true

- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: CopyFiles@2
  displayName: 'Copy Files: $(Build.ArtifactStagingDirectory)'
  inputs:
    SourceFolder: '$(Build.SourcesDirectory)'
    Contents: 'src\Plugins\**\**\bin\**\*.dll'
    TargetFolder: '$(Build.ArtifactStagingDirectory)'
    CleanTargetFolder: true

- task: CopyFiles@2
  displayName: 'Copy CRM Deployment'
  inputs:
    SourceFolder: '$(Build.SourcesDirectory)'
    Contents: 'CrmDeployment\*.*'
    TargetFolder: '$(Build.ArtifactStagingDirectory)'
    CleanTargetFolder: false
    
- task: CmdLine@2
  displayName: 'Deploy All Plugins to CRM'
  inputs:
    script: '$(Build.SourcesDirectory)\CrmDeployment\CrmDeployment.exe $(ConnectionString) Import-Plugins -d $(Build.ArtifactStagingDirectory)'

- task: CmdLine@2
  displayName: 'Export Unmanaged Solutions'
  inputs:
    script: '$(Build.SourcesDirectory)\CrmDeployment\CrmDeployment.exe $(ConnectionString) Export-Solutions -m false -d $(Build.ArtifactStagingDirectory)\CrmSolutions'

- task: CopyFiles@2
  displayName: 'Copy CRM Solutions to Git Folder'
  inputs:
    SourceFolder: '$(Build.ArtifactStagingDirectory)'
    Contents: 'src\CrmSolutions\*.zip'
    TargetFolder: '$(Build.SourcesDirectory)\CrmSolutions'
    CleanTargetFolder: false

- task: CmdLine@2
  inputs:
    script: |
      cd $(Build.SourcesDirectory)
      git show-ref
      git config --global user.email "<youremail>"
      git config --global user.name "<your-username>"
      git add -A $(Build.SourcesDirectory)\CrmSolutions\*.zip
      git commit -m "Update Solution via Azure pipeline.."
      git push -u origin HEAD:master

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'pipelineDevCrm'
    publishLocation: 'Container'

The user.email and user.name in this demo are hardcoded. But you can change it using the Pipeline variable.

Once this is done, the next step is to set up our Project settings to enable self-commit inside the CI/CD process. Click the Project Settings > Repositories > click your repository > Security > select your Users (in my case DevOps Learning Build Service) > Set Contribute, Create tag, Create Branch, and Read as Allow.

Set Security setting

Then the last thing is to create a Variable. Select Pipelines > your pipeline > select Edit > click Variables. Because in the YAML, we defined the variable for exe connection string as ConnectionString, then you need to name it ConnectionString and copy-paste the connection string that we define before:

Create Variables

Once all of this is done, you can test your pipeline:

The result

Please wait for the next flow (deploying to another environment)!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.