Skip to content
PowerPlatform: Source Code and Naming Tips
Albert-Jan Schot
Albert-Jan Schot

· 6 min read

Post

PowerPlatform: Source Code and Naming Tips

As the tech world evolves, so does the need to stay up-to-date with the latest trends and best practices. Ron Jeffries once said, “Code never lies,” and with the new Source Code Integration in Power Platform, I can’t help but agree. This addition gives us the ability to peek into the actual code behind our solutions, which is a game-changer.

I previously blogged about Naming conventions and DevOpts integration, and now with this new feature, I wanted to dive deeper. This post will focus on how you can create a DevOps pipeline that automates the process of checking the naming conventions for your apps. It builds on my previous post and adds new insights.

Setting Up Source Control Integration

With the native source control integration in Power Platform, exporting or unpacking the solution is no longer necessary. If you haven’t set up the source control integration yet, check out the Microsoft guide here: Power Platform Source Code Integration.

Building a pipeline

The first thing we need is a pipeline.yml that we will trigger on each commit. This pipeline will call our script, passing in the folder where the solution is extracted. Here’s an example:

trigger:
- main

pool:
  vmImage: ubuntu-latest

steps:
- checkout: self #checkout source code so it can be used
- task: PowerShell@2 #run the powershell script
  displayName: 'Blis Digital: Naming Validator'
  inputs:
    filePath: '$(Build.SourcesDirectory)/scripts/scoring-script.ps1'
    arguments: '-folderName "PP/ContosoRealEstate" -runAsPipeline $true' #make sure to use the correct folder name

The naming conventions

First lets have a look at our naming conventions, the source is a namingconvention.json and looks like the following:

[
    {
        "Control": "3d object",
        "Naming": "3do"
    },
    {
        "Control": "add picture",
        "Naming": "pic"
    },
...
]

A full list of controls can be found in the Microsoft documentation, but luckily Elianne has provided a full sample.

The improved script

Next, let’s look at the script that will be triggered. The first step is to install the necessary module for handling YAML. The script will then loop through the solution and find all canvas apps, each of which has a canvasapp.yml file.

$canvasApps = Get-ChildItem -Path "$($folderName)\." -Filter canvasapp.yml -Recurse -File

The script will continue by processing each canvasapp.yml file, finding all screens (which are separate files) and recursively checking each element.

function Read-AppElements($element, $screenName) {
    # Check for child elements, if so recursive loop through
    if($element.Keys -eq "Children") {
        if($enableDebug) { Write-output "Has child elements, recursive processing" }

        foreach($child in $element.Children) {
            Read-AppElements $child $screenName
        }
    }
    else {
    # do stuff
    }
}

Check for best practices

We can also add checks for best practices, such as ensuring no OnStart functions are present in the apps. Here’s how we can do that:

if($sourceFile.Name -eq "App.pa.yaml") {
            # Check the OnStart
            if($null -ne $jsonObject.App.Properties.OnStart) {
                $tempElements.Add([PSCustomObject]@{
                    Issue = [IssueType]::BestPractices
                    AppName = $canvasAppDetails.CanvasApp.DisplayName
                    Message = "OnStart should not contain functions, you should use the named formulas instead."
                });

                continue
            }
        }

Given that the structure of the solution is slightly different compared to using the PAC CLI the script contains a few other changes as well,

Here’s the final script that validates naming conventions and checks for best practices:

[CmdletBinding()]
param (
    [Parameter(Mandatory)]
    [string]$folderName,
    [bool]$runAsPipeline=$false,
    [bool]$enableDebug=$false
)

# Set of 'fixed' parameters
$requiredModule = "powershell-yaml"
$namingConventionsPath = "./scripts/namingconvention.json"

# Force Yaml module install due to pipeline being stateless
if (-not (Get-Module -ListAvailable -Name $requiredModule)) {
    Write-Host "$requiredModule is not installed. Installing..."
    Install-Module -Name $requiredModule -Force -Scope CurrentUser
} else {
    Write-Host "$requiredModule is already installed."
}

enum IssueType {
    ControlNaming
    ScreenNaming
    BestPractices
}
$tempElements = [System.Collections.Generic.List[object]]::new()

# Load the naming conventions
$table = Get-Content -Path $namingConventionsPath -Raw | ConvertFrom-Json

# Each CanvasApp has a canvasapp.yml file
write-Host $folderName

$canvasApps = Get-ChildItem -Path "$($folderName)\." -Filter canvasapp.yml -Recurse -File

function Read-AppElements($element, $screenName) {
    # Check for child elements, if so recursive loop through
    if($element.Keys -eq "Children") {
        if($enableDebug) { Write-output "Has child elements, recursive processing" }

        foreach($child in $element.Children) {
            Read-AppElements $child $screenName
        }
    }
    else {
        foreach($key in $element.Keys) {
            if($enableDebug) { Write-Host "Processing element: $($key), $($element[$key].Control), $($screenName)" }

            $tempElements.Add([PSCustomObject]@{
                     Issue = [IssueType]::ControlNaming
                     AppName = $canvasAppDetails.CanvasApp.DisplayName
                     Screen = $screenName
                     Name = $key
                     Type = $element[$key].Control
            });

            if($null -ne $element[$key].Children) {
                if($enableDebug) { Write-output "Has child elements, recursive processing" }

                Read-AppElements $element[$key] $screenName
            }
        }
    }
}

# Loop through all CanvasApps and get details
foreach ($canvasApp in $canvasApps) {
    #  Load App Details
    $canvasAppDetailContent = Get-Content "$($canvasApp.FullName)" -Raw
    $canvasAppDetails = ConvertFrom-Yaml -Yaml $canvasAppDetailContent

    Write-Host "Found app" $canvasAppDetails.CanvasApp.DisplayName

    # Load all source files
    $sourceFiles = Get-ChildItem -Path "$($folderName)/$($canvasAppDetails.CanvasApp.DocumentUri.split('.')[0])/Src" -Filter *.yaml -Recurse -File

    foreach ($sourceFile in $sourceFiles) {
        if($enableDebug) { Write-output "$($sourceFile.FullName)" }

        $fileContent = Get-Content "$($sourceFile.FullName)" -Raw
        $jsonObject = ConvertFrom-Yaml -Yaml $fileContent

        # Check for App best practices
        if($sourceFile.Name -eq "App.pa.yaml") {
            # Check the OnStart
            if($null -ne $jsonObject.App.Properties.OnStart) {
                $tempElements.Add([PSCustomObject]@{
                    Issue = [IssueType]::BestPractices
                    AppName = $canvasAppDetails.CanvasApp.DisplayName
                    Message = "OnStart should not contain functions, you should use the named formulas instead."
                });

                continue
            }
        }

        # Loop through the screens and start processing
        foreach($key in $jsonObject.Screens.Keys) {
            Write-Host "Processing screen: $($key)"

            Read-AppElements $jsonObject.Screens[$key] $key
        }
    }
}

## Loop through all elements and check if they are in the naming conventions
## Then handle the naming conventions itself
foreach ($item in $tempElements | Where-Object { $_.Issue -eq [IssueType]::BestPractices }) {
    $message = "$($item.Message)"
    $status = "Warning"
    Write-Host "❌ " $message -ForeGroundColor Blue
    if($runAsPipeline) { Write-Host "##vso[task.logissue type=warning;]" $message }
}

foreach ($item in $tempElements | Where-Object { $_.Issue -eq [IssueType]::ControlNaming }) {
    $naming = $table | Where-Object { $_.Control -contains $item.Type.split('@')[0] } | Select-Object -ExpandProperty Naming

    if ($naming -and $item.Name -cmatch "^$naming") {
        $message = "Control '$($item.Name)' matches type '$($item.Type)' with naming convention '$($naming)'."
        $status = "Correct"
        Write-Host "✅ " $message
    }
    elseif ($item.Type -eq "screen" -and $item.Name -cmatch 'Screen$') {
        $message = "Screen $($item.Name) matches naming convention."
        $status = "Correct"
        Write-Host "✅ " $message
    }
    elseif ($item.Type -eq "screen" -and $item.Name -notmatch 'Screen$') {
        $message = "Screen $($item.Name) doesn't match naming convention."
        $status = "Error"
        Write-Host "❌ " $message -ForeGroundColor Blue
        if($runAsPipeline) { Write-Host "##vso[task.logissue type=warning;]" $message }
    }
    else {
        $message = "Control '$($item.Name)' with type '$($item.Type)' does not match naming convention, Name should start with '$($naming)' on Screen named '$($item.Screen)' $(if ( $item.Path -ne '') { "in element '$($item.Path)'"})"
        $status = "Error"
        Write-Host "❌ " $message -ForegroundColor Blue
        if($runAsPipeline) { Write-Host "##vso[task.logissue type=warning;]" $message }
    }
}

Conclusion

Integrating naming convention checks into your DevOps pipeline for Power Platform solutions ensures consistency across the board. By automating this process, you can catch issues early and maintain a high level of governance in your development process. With the new source code integration feature in Power Platform, this becomes even more seamless.

Albert-Jan Schot

Albert-Jan Schot

CTO, Microsoft MVP & FastTrack Recognized Solution Architect

I am Albert-Jan Schot, CTO at Blis Digital, Microsoft MVP, and FastTrack Recognized Solution Architect focused on Microsoft 365, Azure, and AI agents. I help teams turn complex Microsoft Cloud challenges into practical architecture decisions and shipped outcomes.

Copilot Studio Microsoft 365 Agent Flows

Zuid Holland, Netherlands

Related Posts