Power Platform ALM: How to Manage Solution Deployments with GitHub Pipelines

Using the pipeline we created in the previous article, I’ll document the steps to commit solutions to GitHub.

You can find the previous article here:
Power Platform ALM: Building Basic Solution Deployment Pipelines - Step by Step Guide
I had an opportunity to use "Pipelines," a new management tool in Power Platform, so I'll document how to use it.What We...
スポンサーリンク

GitHub Commit Integration

We’ll extend the Power Platform deployment pipeline we created previously to commit solutions to GitHub. While the process generally follows the official documentation, some modifications are necessary.

Official Documentation:
Extend pipelines using GitHub Actions - Power Platform
Download, unpack, and commit a solution using a GitHub workflow called from a Power Automate Flow.

Implementation Steps

Follow these steps to set up the integration:
  1. Register an application in Entra ID (formerly Azure AD)
  2. Add an application user to your Power Platform environment
  3. GitHub: Create a repository and set up Actions
  4. GitHub: Generate an access token
  5. Build Power Automate flow: Configure GitHub API calls

Entra ID Registration

First, let’s start with Entra ID registration. During this process, make note of these three values:
  • Client Secret
  • Client ID (Application ID)
  • Tenant ID
Begin by opening Entra ID in the Azure Portal
Select [App registrations] and click [New registration]
Enter an appropriate name and register it as a single-tenant application.
Next, go to [API permissions] of the registered app and select [Dynamics CRM]
Check “user_impersonation” and click [Add permissions]
Go to [Certificates & secrets] and select [New client secret]
Make note of the generated value
Finally, go to [Overview] and note down both the [Application ID] and [Tenant ID]. This completes this section.

Registering an Application User in Power Platform Environment

Next, we’ll create an application user in the Power Platform environment and assign Dataverse security roles for GitHub integration.
In the Power Platform admin center, select the environment where you built the pipeline and click [Settings].
Select [Application users]
Click [New app user], then [Add an app]
Select the application you registered in Entra ID from the previous steps.
Set the business unit (in this example, we’re using the root business unit) and security role, then click [Create].
Note: While the System Customizer role will definitely work, it’s recommended to create a custom role if you want to minimize permissions.
This completes the application user registration in your Power Platform environment.

GitHub: Creating Repository and Setting Up Actions

Repository Creation

Navigate to GitHub and create a new repository.

Actions Configuration

After creating the repository, go to Actions and select [set up a workflow yourself].
Then, paste the following modified version of the official code and click [Commit changes]. This code includes our specific modifications to the original.
The code modifications are as follows:
# Before
      # Unpack the solution
      - name: unpack solution
        uses: microsoft/powerplatform-actions/unpack-solution@v0
        with:
          solution-file: "${{ github.event.inputs.solution_name }}.zip"
          solution-folder: "${{ github.event.repository.name }}"
          solution-type: 'Both'
          process-canvas-apps: false
          overwrite-files: true

# After the change (v1 was used because the unpack-solution did not work with v0)
      # Install PAC    
      - name: Install Power Platform Tools
        uses: microsoft/powerplatform-actions/actions-install@v1
        
      # Unpack the solution
      - name: unpack solution
        uses: microsoft/powerplatform-actions/unpack-solution@v1
        with:
          solution-file: "${{ github.event.inputs.solution_name }}.zip"
          solution-folder: "${{ github.event.repository.name }}"
          solution-type: 'Both'
          process-canvas-apps: false
          overwrite-files: true
Below is the full source code.
name: Download, unpack and commit the solution to git
run-name: Getting ${{ github.event.inputs.solution_name }} from pipelines host environment and committing
on:
  workflow_dispatch:
    inputs:
      artifact_url:
        description: "The url of the Dataverse record ID for the artifact created by the pipelines (Example: https://[your-env].crm.dynamics.com/api/data/v9.0/deploymentartifacts([your-artifact-id])/artifactfile/$value)."
        required: true
      solution_name:
        description: "Name of the Solution in Dataverse environment"
        required: true
      user_name: 
        description: "User name for the commit"
        required: true
      source_branch:
        description: "Branch for the solution commit"
        required: true
      target_branch:
        description: "Branch to create for the solution commit"
        required: false
      commit_message:
        description: "Message to provide for the commit"
        required: true
permissions:
  contents: write
jobs:
  export-unpack-commit:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
        with:
            ref: ${{ github.event.inputs.source_branch }}

      # Commit changes to the existing or new branch
      - name: create new branch if specified
        shell: pwsh
        run: |
            if('${{ github.event.inputs.target_branch }}' -ne '') {
                git checkout -b ${{ github.event.inputs.target_branch }} ${{ github.event.inputs.source_branch }}
            }

      # Export the solution from the artifact created by pipelines
      - name: download solution from artifact
        env:
            CLIENT_ID: ${{secrets.CLIENT_ID}}   
            TENANT_ID: ${{secrets.TENANT_ID}}   
            CLIENT_SECRET: ${{secrets.CLIENT_SECRET}}
        shell: pwsh
        run: |
            $aadHost = "login.microsoftonline.com"
            $url = "${{ github.event.inputs.artifact_url }}"
            $options = [System.StringSplitOptions]::RemoveEmptyEntries
            $dataverseHost = $url.Split("://", $options)[1].Split("/")[0]

            $body = @{client_id = $env:CLIENT_ID; client_secret = $env:CLIENT_SECRET; grant_type = "client_credentials"; scope = "https://$dataverseHost/.default"; }
            $OAuthReq = Invoke-RestMethod -Method Post -Uri "https://$aadHost/$env:TENANT_ID/oauth2/v2.0/token" -Body $body
            $spnToken = $OAuthReq.access_token
            $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
            $headers.Add("Authorization", "Bearer $spnToken")
            $headers.Add("Content-Type", "application/json")

            # Download the managed solution
            $response = Invoke-RestMethod "${{ github.event.inputs.artifact_url }}" -Method 'GET' -Headers $headers
            $bytes = [Convert]::FromBase64String($response.value)
            [IO.File]::WriteAllBytes("${{ github.event.inputs.solution_name }}_managed.zip", $bytes)

            # Download the unmanaged solution (for now we will need to use string manipulation to get the unmanaged solution URL, until the API provides this value)
            $unmanaged_artifact_url = "${{ github.event.inputs.artifact_url }}".Replace("artifactfile", "artifactfileunmanaged")
            $response = Invoke-RestMethod "$unmanaged_artifact_url" -Method 'GET' -Headers $headers
            $bytes = [Convert]::FromBase64String($response.value)
            [IO.File]::WriteAllBytes("${{ github.event.inputs.solution_name }}.zip", $bytes)

      # Install PAC    
      - name: Install Power Platform Tools
        uses: microsoft/powerplatform-actions/actions-install@v1
        
      # Unpack the solution
      - name: unpack solution
        uses: microsoft/powerplatform-actions/unpack-solution@v1
        with:
          solution-file: "${{ github.event.inputs.solution_name }}.zip"
          solution-folder: "${{ github.event.repository.name }}"
          solution-type: 'Both'
          process-canvas-apps: false
          overwrite-files: true

      # Commit changes to the existing or new branch
      - name: commit changes
        shell: pwsh
        run: |
          rm -rf ${{ github.event.inputs.solution_name }}.zip
          rm -rf ${{ github.event.inputs.solution_name }}_managed.zip
          git config user.name ${{ github.event.inputs.user_name }}
          git pull 
          git add --all
          git commit -am "${{ github.event.inputs.commit_message }}" --allow-empty

      # Push the committed changes to the source branch
      - name: push to branch
        shell: pwsh
        run: |
          if('${{ github.event.inputs.target_branch }}' -ne '') {
              git push origin ${{ github.event.inputs.target_branch }}
          } else {
              git push origin ${{ github.event.inputs.source_branch }}
          }

Registering Repository Secrets

Open [Settings] in your repository, navigate to [Secrets and variables] > [Actions], and select [New repository secret].
Configure the three values you noted during Entra ID setup:
  • CLIENT_ID: Your Client ID
  • CLIENT_SECRET: Your Secret value
  • TENANT_ID: Your Tenant ID
This completes the repository and Actions configuration setup.

GitHub: Creating Access Token

Open your GitHub tenant (user) settings, navigate to [Developer Settings] > [Personal access tokens] > [Fine-grained tokens], and click [Generate new token].
Set an appropriate name and token expiration date
Limit the Repository access to the repository you just created
Under Permissions, set both [Actions] and [Contents] to “Read and Write”
Click [Generate] and make note of the GitHub access token that is generated
This completes the access token creation process.

Power Automate Setup: GitHub API Call

Finally, let’s build the Power Automate flow that calls the GitHub Actions we created. Here’s the overall flow diagram:
First, configure the trigger as shown below:
Note: While the official reference mentions “OnDeploymentRequested”, this might cause issues in some cases. For now, we’ll use this configuration.
Next, use the “Get row by ID” action to retrieve a row from the “Deployment Stage Execution” table.
Finally, use the HTTP action to POST to the following URL:
URL: ~~~/repos/[Tenant(Username)]/[RepositoryName]/actions/workflows/[WorkflowFileName]/dispatches
Header: Authorization : Bearer [GitHub Access Token]


Configure the Body as shown below.
Note: Adjust the source_branch and target_branch settings according to your needs.
{
  "ref": "main",
  "inputs": {
    "artifact_url": "@{triggerOutputs()?['body/OutputParameters/ArtifactFileDownloadLink']}",
    "solution_name": "@{triggerOutputs()?['body/OutputParameters/ArtifactName']}",
    "user_name": "@{triggerOutputs()?['body/OutputParameters/DeployAsUser']}",
    "source_branch": "main",
    "commit_message": "@{outputs('Get row by ID')?['body/deploymentnotes']}"
  }
}
Although it was a bit lengthy, this completes the entire setup process.

Operation Verification

Change the label content of the canvas app in the solution created in the previous article to v1.0.0.4, then save and publish.
After publishing, when you trigger the configured pipeline,
Power Automate executes and calls the GitHub Actions.
After a short wait, the GitHub action completes successfully,
And the solution contents are committed to GitHub.
Since we modified the canvas app content, we can confirm that the .msapp file has been successfully updated.

Actions May Sometimes Fail

When running the pipeline using exactly the same steps,
While Power Automate executes successfully,
GitHub Actions may sometimes fail.
As indicated by the error message “No file attachment found for attribute”, this likely occurs when the GitHub action attempts to retrieve the managed solution zip file before the pipeline has completed the solution packaging process.

コメント

Copied title and URL