You are currently viewing Automatic Code Review of Power Automate Flows with Power Platform Pipelines

Automatic Code Review of Power Automate Flows with Power Platform Pipelines

In my previous post, I integrated a power apps code review tool with Power Platform Pipelines. The tool that the Power CAT team had built was amazing and gave me some ideas on how I could handle doing reviews of Power Automate flows.

 

Building off of the last process, I have added three flows. 1.2.2 that will start the flows check process, 1.2.2.1 that will check for blank descriptions, and 1.2.2.2 that checks for default names

flowchart TD A[1.2 - When a push to test happens] -->|Solution Name & Solution Version & Stage Run ID & ArtifactID| B[1.2.1 - Start App Review] A--> |StageRunID & ArtifactID| C[1.2.2 - Start Flow Review] C --> |CloudFlows & Counter| D[1.2.2.1 - Flow Blank Descriptions Check] C --> |CloudFlows| E[1.2.2.2 - Default Name Check]

 

erDiagram "Power Automate Code Rules"}| -- || "Code Review Scoring":contains "Power Automate Code Reviews"{ GUID CodeReview PK dec DescriptionScore dec DefaultNameScore dec FinalScore } "Power Automate Code Rules"{ string Rulename PK GUID CodeReviewScoring FK string Rule } "Code Review Scoring"{ string Scorename PK dec Rating }

Β 

πŸ’‘ The column FinalScore is a PowerFX column that adds the two scores then divides them by two

('Description Score' + 'Default Name Score') / 2

 

 

All three tables are added to an MDA for management

 

Review Scorings

There are six Review Scores in the table

Review Rules

Two review rules are added into the table

 

The default action names will have a list of default action names and have a score of Low

 

The blank descriptions rule will be set to null and a score of informational

1.2.2 - Start Flow Review

  1. A manual trigger that calls for the solution version and the solution name followed by three variables
    1. varCloudFlows - Array
    2. varCounter - Integer
    3. varFlowDesctiptionsArray - Array
  2. A Scope with three actions
    1. The first action will list all rows from the solutions table in the development environment using a FetchXML query and the SolutionName from the trigger
      <fetch>
        <entity name="solution">
          <attribute name="friendlyname" />
          <attribute name="solutionid" />
          <attribute name="uniquename" />
          <filter>
            <condition attribute="uniquename" operator="eq" value="@{triggerBody()?['text']}" />
          </filter>
        </entity>
      </fetch>

       

    2. Next will be another list all rows from the solution components table in development using a FetchXML
      <fetch>
        <entity name="solutioncomponent">
          <attribute name="objectid" />
          <filter type="and">
            <condition attribute="solutionid" operator="eq" value="@{outputs('List_rows_from_selected_environment_-_Solutions')?['body/value']?[0]?['solutionid']}" />
          </filter>
          <filter>
            <condition attribute="componenttype" operator="eq" value="29" />
          </filter>
        </entity>
      </fetch>

       

      πŸ’‘ I am filtering the component type by 29 (workflows)

    3. For each workflow a get row by id in the process table from the development environment
    4. A condition that checks if the process is a modern cloud flow

      πŸ’‘

    5. If it is a modern flow, then it is added to the array varCloudFlows and increments the counter by 1
  3. Two child flows are then triggered

     

  4. After the 1.2.2.1 a scope is added with three actions
  5. The Parse JSON will convert a string that is returned by the 1.2.2.1 flow back to an array
    {
        "type": "array",
        "items": {
            "type": "object",
            "properties": {
                "ProcessGUID": {
                    "type": "object",
                    "properties": {
                        "@@odata.type": {
                            "type": "string"
                        },
                        "@@odata.id": {
                            "type": "string"
                        },
                        "@@odata.etag": {
                            "type": "string"
                        },
                        "@@odata.editLink": {
                            "type": "string"
                        },
                        "objectid@odata.type": {
                            "type": "string"
                        },
                        "objectid": {
                            "type": "string"
                        },
                        "solutioncomponentid@odata.type": {
                            "type": "string"
                        },
                        "solutioncomponentid": {
                            "type": "string"
                        }
                    }
                },
                "ProcessName": {
                    "type": "string"
                },
                "ProcessDescription": {},
                "DescriptionScore": {
                    "type": "number"
                }
            },
            "required": [
                "ProcessGUID",
                "ProcessName",
                "ProcessDescription",
                "DescriptionScore"
            ]
        }
    }

     

  6. For each of the items out of the Parse JSON an item is added to the array varFlowDesctiptionsArray

    πŸ’‘ For the Process Description an if expression is used. If it is blank then Description Missing is added

    if(empty(item()?['ProcessDescription']), 'Description Missing', item()?['ProcessDescription'])
    

     

  7. This array is then converted to a HTML table using Automatic column headers
  8. The last two action are a add a new row to Code Reviews and respond to a flow
  9. The add new row will be configured as follows

    πŸ’‘ This add is optional. If you do not want the outputs to be stored in both the pipeline environment and the production environment you can skip this.

  10. The last step will be to respond to the parent flow
{
  "type": "Response",
  "kind": "PowerApp",
  "inputs": {
    "schema": {
      "type": "object",
      "properties": {
        "defaultnamescore": {
          "title": "DefaultNameScore",
          "type": "number",
          "x-ms-content-hint": "NUMBER",
          "x-ms-dynamically-added": true
        },
        "defaultnamedescriptions": {
          "title": "DefaultNameDescriptions",
          "type": "string",
          "x-ms-content-hint": "TEXT",
          "x-ms-dynamically-added": true
        },
        "descriptionscore": {
          "title": "DescriptionScore",
          "type": "number",
          "x-ms-content-hint": "NUMBER",
          "x-ms-dynamically-added": true
        },
        "descriptionsoutput": {
          "title": "Descriptionsoutput",
          "type": "string",
          "x-ms-content-hint": "TEXT",
          "x-ms-dynamically-added": true
        }
      },
      "additionalProperties": {}
    },
    "statusCode": 200,
    "body": {
      "defaultnamescore": "@body('Run_a_Child_Flow_1.2.2.2')?['defaultnamescore']",
      "defaultnamedescriptions": "@{body('Run_a_Child_Flow_1.2.2.2')?['defaultnameactions']}",
      "descriptionscore": "@body('Run_a_Child_Flow_-_1.2.2.1')?['score']",
      "descriptionsoutput": "@{body('Run_a_Child_Flow_-_1.2.2.1')?['descriptions']}"
    }
  },
  "runAfter": {
    "Add_a_new_row_-_Code_Reviews": [
      "Succeeded"
    ]
  },
  "metadata": {
    "operationMetadataId": "18d71153-94ce-447e-94a9-a8468d37cadd"
  }
}

 

1.2.2.1 - Flow Blank Descriptions Check

  1. A manual trigger followed by a Parse JSON and two variables
    {
        "type": "array",
        "items": {
            "type": "object",
            "properties": {
                "ProcessGUID": {
                    "type": "string"
                },
                "ProcessName": {
                    "type": "string"
                },
                "ProcessClientData": {
                    "type": "string"
                },
                "ProcessDescription": {}
            },
            "required": [
                "ProcessGUID",
                "ProcessName",
                "ProcessClientData",
                "ProcessDescription"
            ]
        }
    }

     

  2. A scope with two actions is next
    1. The first will list the rows and find the rule for blank descriptions and gets its’ subsequent score using FetchXML
      <fetch>
        <entity name="andy_powerautomatereviewrules">
          <filter>
            <condition attribute="andy_rulename" operator="eq" value="Description is blank" />
          </filter>
          <link-entity name="andy_codereviewscoring" from="andy_codereviewscoringid" to="andy_score" alias="ScoreOutput">
            <attribute name="andy_rating" />
            <attribute name="andy_scorename" />
          </link-entity>
        </entity>
      </fetch>

       

    2. For workflow the description will be checked if it is blank
    3. If its blank it will be added to the variable varDescScores
      {
        "type": "AppendToArrayVariable",
        "inputs": {
          "name": "varDescScores",
          "value": {
            "ProcessGUID": "@items('Apply_to_each_-_Check_if_workflow_is_blank')?['ProcessGUID']",
            "ProcessName": "@items('Apply_to_each_-_Check_if_workflow_is_blank')?['ProcessName']",
            "ProcessDescription": "@items('Apply_to_each_-_Check_if_workflow_is_blank')?['ProcessDescription']",
            "DescriptionScore": "@outputs('List_rows_-_Blank_Review_Rule')?['body/value']?[0]?['ScoreOutput.andy_rating']"
          }
        },
        "metadata": {
          "operationMetadataId": "00665194-289c-4c09-a229-46824ca3a74c"
        }
      }

       

    4. Then the score is added to the variable varDescScore
    5. If there is a description it will do the same thing, expect for the score f
  3. It will then respond to the parent flow with the overall score and descriptions array
  4. The Score will divide the sum of all the scores by the amount of flows (Counter)
    div(variables('varDescScore'),triggerBody()?['number'])

     

1.2.2.2 - Default Name Check

This flow was one of the hardest flows I have made. To get the names of the actions out of the flow was a brain teaser. Each of the actions names are stored within the client data column of the process table. It is stored as an array, but one that proved difficult to access the elements I wanted.

  1. A manual trigger followed by 4 variables and a parse json is added
    1. varDefault Names - Array
    2. varDefaultNameCounter - Interger
    3. varDefaultNameScore - Float
    4. varDefaultNamesScores - Array
    5. Parse JSON
      {
          "type": "array",
          "items": {
              "type": "object",
              "properties": {
                  "ProcessGUID": {
                      "type": "string"
                  },
                  "ProcessName": {
                      "type": "string"
                  },
                  "ProcessClientData": {
                      "type": "string"
                  },
                  "ProcessDescription": {}
              },
              "required": [
                  "ProcessGUID",
                  "ProcessName",
                  "ProcessClientData",
                  "ProcessDescription"
              ]
          }
      }

       

  2. A scope with four actions is then added
    1. The first action lists all the rules and the scores using a FetchXML query
      <fetch>
        <entity name="andy_powerautomatereviewrules">
          <attribute name="andy_rule" />
          <filter>
            <condition attribute="andy_rulename" operator="eq" value="Default Action Names" />
          </filter>
          <link-entity name="andy_codereviewscoring" from="andy_codereviewscoringid" to="andy_score" alias="ScoreOutput">
            <attribute name="andy_rating" />
            <attribute name="andy_scorename" />
          </link-entity>
        </entity>
      </fetch>

       

    2. The second parses the JSON for the rules
      outputs('List_rows_-_Get_Default_Name_Rules')?['body/value']?[0]?['andy_rule']

       

      {
          "type": "array",
          "items": {
              "type": "string"
          }
      }

       

    3. Each rule is then added to the array variable varDefaultNames
    4. A for loop is added for each workflow
      body('Parse_JSON_-_CloudFlows')

       

      1. The first step in the loop is to parse the JSON of the client data
        {
            "type": "object",
            "properties": {
                "properties": {
                    "type": "object",
                    "properties": {
                        "connectionReferences": {
                            "type": "object",
                            "properties": {}
                        },
                        "definition": {
                            "type": "object",
                            "properties": {
                                "$schema": {
                                    "type": "string"
                                },
                                "contentVersion": {
                                    "type": "string"
                                },
                                "parameters": {
                                    "type": "object",
                                    "properties": {
                                        "$authentication": {
                                            "type": "object",
                                            "properties": {
                                                "defaultValue": {
                                                    "type": "object",
                                                    "properties": {}
                                                },
                                                "type": {
                                                    "type": "string"
                                                }
                                            }
                                        }
                                    }
                                },
                                "triggers": {
                                    "type": "object",
                                    "properties": {
                                        "manual": {
                                            "type": "object",
                                            "properties": {
                                                "metadata": {
                                                    "type": "object",
                                                    "properties": {
                                                        "operationMetadataId": {
                                                            "type": "string"
                                                        }
                                                    }
                                                },
                                                "type": {
                                                    "type": "string"
                                                },
                                                "kind": {
                                                    "type": "string"
                                                },
                                                "inputs": {
                                                    "type": "object",
                                                    "properties": {
                                                        "schema": {
                                                            "type": "object",
                                                            "properties": {
                                                                "type": {
                                                                    "type": "string"
                                                                },
                                                                "properties": {
                                                                    "type": "object",
                                                                    "properties": {
                                                                        "text": {
                                                                            "type": "object",
                                                                            "properties": {
                                                                                "title": {
                                                                                    "type": "string"
                                                                                },
                                                                                "type": {
                                                                                    "type": "string"
                                                                                },
                                                                                "x-ms-dynamically-added": {
                                                                                    "type": "boolean"
                                                                                },
                                                                                "description": {
                                                                                    "type": "string"
                                                                                },
                                                                                "x-ms-content-hint": {
                                                                                    "type": "string"
                                                                                }
                                                                            }
                                                                        },
                                                                        "file": {
                                                                            "type": "object",
                                                                            "properties": {
                                                                                "title": {
                                                                                    "type": "string"
                                                                                },
                                                                                "type": {
                                                                                    "type": "string"
                                                                                },
                                                                                "x-ms-dynamically-added": {
                                                                                    "type": "boolean"
                                                                                },
                                                                                "description": {
                                                                                    "type": "string"
                                                                                },
                                                                                "x-ms-content-hint": {
                                                                                    "type": "string"
                                                                                },
                                                                                "properties": {
                                                                                    "type": "object",
                                                                                    "properties": {
                                                                                        "name": {
                                                                                            "type": "object",
                                                                                            "properties": {
                                                                                                "type": {
                                                                                                    "type": "string"
                                                                                                }
                                                                                            }
                                                                                        },
                                                                                        "contentBytes": {
                                                                                            "type": "object",
                                                                                            "properties": {
                                                                                                "type": {
                                                                                                    "type": "string"
                                                                                                },
                                                                                                "format": {
                                                                                                    "type": "string"
                                                                                                }
                                                                                            }
                                                                                        }
                                                                                    }
                                                                                }
                                                                            }
                                                                        }
                                                                    }
                                                                },
                                                                "required": {
                                                                    "type": "array",
                                                                    "items": {
                                                                        "type": "string"
                                                                    }
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                },
                                "actions": {
                                    "type": "object",
                                    "properties": {
                                        "Compose": {
                                            "type": "object",
                                            "properties": {
                                                "runAfter": {
                                                    "type": "object",
                                                    "properties": {}
                                                },
                                                "metadata": {
                                                    "type": "object",
                                                    "properties": {
                                                        "operationMetadataId": {
                                                            "type": "string"
                                                        }
                                                    }
                                                },
                                                "type": {
                                                    "type": "string"
                                                },
                                                "inputs": {
                                                    "type": "string"
                                                }
                                            }
                                        },
                                        "Respond_to_a_Power_App_or_flow": {
                                            "type": "object",
                                            "properties": {
                                                "runAfter": {
                                                    "type": "object",
                                                    "properties": {
                                                        "Compose": {
                                                            "type": "array",
                                                            "items": {
                                                                "type": "string"
                                                            }
                                                        }
                                                    }
                                                },
                                                "metadata": {
                                                    "type": "object",
                                                    "properties": {
                                                        "operationMetadataId": {
                                                            "type": "string"
                                                        }
                                                    }
                                                },
                                                "type": {
                                                    "type": "string"
                                                },
                                                "kind": {
                                                    "type": "string"
                                                },
                                                "inputs": {
                                                    "type": "object",
                                                    "properties": {
                                                        "statusCode": {
                                                            "type": "integer"
                                                        },
                                                        "body": {
                                                            "type": "object",
                                                            "properties": {
                                                                "data": {
                                                                    "type": "string"
                                                                }
                                                            }
                                                        },
                                                        "schema": {
                                                            "type": "object",
                                                            "properties": {
                                                                "type": {
                                                                    "type": "string"
                                                                },
                                                                "properties": {
                                                                    "type": "object",
                                                                    "properties": {
                                                                        "data": {
                                                                            "type": "object",
                                                                            "properties": {
                                                                                "title": {
                                                                                    "type": "string"
                                                                                },
                                                                                "x-ms-dynamically-added": {
                                                                                    "type": "boolean"
                                                                                },
                                                                                "type": {
                                                                                    "type": "string"
                                                                                }
                                                                            }
                                                                        }
                                                                    }
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        },
                        "templateName": {
                            "type": "string"
                        }
                    }
                },
                "schemaVersion": {
                    "type": "string"
                }
            }
        }

         

      2. Then to clean the output into a useable array. This will all be stored within a scope action
        1. The Compose will remove all β€œ@” from the output and replace it with a β€œat”. This is needed for when we convert it with xpath.
          replace(string(body('Parse_JSON_-_Client_Data')?['properties']?['definition']?['actions']), '@', 'at_')
          

           

        2. The string is then converted into a JSON
          json(outputs('Compose_-_Clean_to_String'))

           

        3. The output is then converted to an array
      3. An for loop of each action is then added

        πŸ’‘ Yes I know that this is a nested for loop, and thats not great. But sometimes is inevitable.

        xpath(xml(outputs('Compose_-_Create_Array')),'/data/*')

         

        πŸ’‘ For a great read on xpath check out Tom Riha’s blog. I could not have done it without it. How to extract value from XML using Power Automate flow

        1. The first step will check to see if the name is a default name
          contains(variables('varDefaultNames'), xpath(item(), 'name(/*)'))

           

        2. The second increments the variable varDefaultNameCounter by 1
        3. The next is a condition that checks if the output of the compare is true
        4. If true or false two actions will occur
          1. If True the variable varDefaultNameScores will be appended
            {
              "ProcessGUID": @{items('Apply_to_each_-_Default_Names')?['ProcessGUID']},
              "ProcessName": @{items('Apply_to_each_-_Default_Names')?['ProcessName']},
              "Action": @{xpath(item(),'name(/*)')},
              "DefaultNameScore": @{outputs('List_rows_-_Get_Default_Name_Rules')?['body/value']?[0]?['ScoreOutput.andy_rating']}
            }

             

          2. If True the variable varDefaultNameScore will be incremented
            outputs('List_rows_-_Get_Default_Name_Rules')?['body/value']?[0]?['ScoreOutput.andy_rating']

             

          3. If False the variable varDefaultNameScores will be appended
            {
              "ProcessGUID": @{items('Apply_to_each_-_Default_Names')?['ProcessGUID']},
              "ProcessName": @{items('Apply_to_each_-_Default_Names')?['ProcessName']},
              "Action": @{xpath(item(),'name(/*)')},
              "DefaultNameScore": 1
            }

             

          4. If False the variable varDefaultNameScore will be incremented
  3. Finally the flow will be closed out with a response
    div(variables('varDefaultNameScore'), variables('varDefaultNameCounter'))

     

    string(variables('varDefaultNameScores'))

     

 

1.2 - When a push to test happens flow updates

  1. Back in the parent flow that was built in the previous post, add the 1.2.2 flow to run alongside the 1.2.1 flow.
  2. Two actions are added. A new variable varFlowDesctiptionsArray is added and a scope to format the output
  3. Within the scope are two other scopes
  4. The Scope Descriptions will have three Actions in it
    1. The Parse JSON will parse the description output
      {
          "type": "array",
          "items": {
              "type": "object",
              "properties": {
                  "ProcessGUID": {
                      "type": "object",
                      "properties": {
                          "@@odata.type": {
                              "type": "string"
                          },
                          "@@odata.id": {
                              "type": "string"
                          },
                          "@@odata.etag": {
                              "type": "string"
                          },
                          "@@odata.editLink": {
                              "type": "string"
                          },
                          "objectid@odata.type": {
                              "type": "string"
                          },
                          "objectid": {
                              "type": "string"
                          },
                          "solutioncomponentid@odata.type": {
                              "type": "string"
                          },
                          "solutioncomponentid": {
                              "type": "string"
                          }
                      }
                  },
                  "ProcessName": {
                      "type": "string"
                  },
                  "ProcessDescription": {},
                  "DescriptionScore": {
                      "type": "number"
                  }
              },
              "required": [
                  "ProcessGUID",
                  "ProcessName",
                  "ProcessDescription",
                  "DescriptionScore"
              ]
          }
      }

       

    2. For each description the variable varFlowDesctiptionsArray is appeneded
      if(empty(item()?['ProcessDescription']), 'Description Missing', item()?['ProcessDescription'])
      

       

    3. An HTML table is then created usign the variable varFlowDesctiptionsArray
  5. The Default Names Scope will have three actions
    1. The Compose will create the JSON out of the output
      json()body('Run_a_Child_Flow_-_1.2.2')?['defaultnamedescriptions'])

       

    2. The Select will create the table structure
      {
        "type": "Select",
        "inputs": {
          "from": "",
          "select": "{n  "Flow Name": ,n  "Action Name": ,n  "Action Score": n}"
        },
        "runAfter": {
          "Compose_-_Create_JSON": [
            "SUCCEEDED"
          ]
        },
        "metadata": {
          "operationMetadataId": "43e873e4-512d-4890-b495-a4eac9d66236"
        }
      }

       

    3. The Create HTML table will create the actual HTML table from the output of the select
  6. These updates are then all added to the history table within production
{
  "type": "OpenApiConnection",
  "inputs": {
    "parameters": {
      "organization": "https://andworx-production.crm.dynamics.com",
      "entityName": "andy_powerplatformsolutionshistories",
      "item/andy_solutionversion": "@outputs('Get_a_row_by_ID_-_Deployment_Artifacts')?['body/artifactversion']",
      "item/andy_appreviewscore": "@body('Run_a_Child_Flow_-_1.2.1')?['appreviewscore']",
      "item/andy_automatedefaultnamearrayoutput": "@body('Run_a_Child_Flow_-_1.2.2')?['defaultnamedescriptions']",
      "item/andy_automatedefaultnamescore": "@body('Run_a_Child_Flow_-_1.2.2')?['defaultnamescore']",
      "item/andy_automatedefaultnametableoutput": "@body('Create_HTML_table_-_Default_Names')",
      "item/andy_automatedescriptionscore": "@body('Run_a_Child_Flow_-_1.2.2')?['descriptionscore']",
      "item/andy_flowdescriptionsarrayoutput": "@body('Run_a_Child_Flow_-_1.2.2')?['defaultnamedescriptions']",
      "item/andy_flowdescriptionstableoutput": "@body('Create_HTML_table_-_varFlowDesctiptionsArray')",
      "item/andy_linktoreport": "https://apps.powerapps.com/play/@{parameters('Power Apps Review App ID (andy_PowerAppsReviewAppID)')}?reviewid=@{body('Run_a_Child_Flow_-_1.2.1')?['solutionreview']}",
      "item/andy_Solution@odata.bind": "andy_powerplatformsolutionstrackings(@{outputs('List_rows_from_selected_environment_-_Solution_Tracking')?['body/value']?[0]?['andy_powerplatformsolutionstrackingid']})"
    },
    "host": {
      "apiId": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps",
      "connection": "shared_commondataserviceforapps",
      "operationId": "CreateRecordWithOrganization"
    }
  },
  "runAfter": {
    "List_rows_from_selected_environment_-_Solution_Tracking": [
      "Succeeded"
    ]
  },
  "metadata": {
    "operationMetadataId": "4cbf99ac-60fd-4696-ac0d-f7c112f48930"
  }
}

 

Final Report

Putting it all together will give us a snapshot into how our solution currently looks

πŸ’‘ The tables are built using a bit of JavaScript and a iframe from the html table created earlier

πŸ’‘ The app is loaded using an iframe and JavaScript from the link that was created earlier

 

Leave a Reply