Skip to content

Commit

Permalink
PowerPlatform integration (#927)
Browse files Browse the repository at this point in the history
Fixes #361

New Settings:
- PowerPlatformSolutionFolder - containing the name of the folder
containing a PowerPlatform Solution (only one)
- companyId and ppEnvironmentUrl added to deploymentSettings for
environments.

New Actions:
- BuildPowerPlatform - to build a PowerPlatform Solution
- DeployPowerPlatform - to deploy a PowerPlatform Solution
- PullPowerPlatformChanges - to pull changes made in PowerPlatform
studio into the repository
- ReadPowerPlatformSettings - to read settings and secrets for
PowerPlatform deployment
- GetArtifactsForDeployment - originally code from deploy.ps1 to
retrieve artifacts for releases or builds - now as an action to read
apps into a folder.

New Workflows:
- Pull PowerPlatform Changes
- Push PowerPlatform Changes

Other changes
- Getting artifacts for deployment moved from deploy.ps1 to a seperate
action
- Test for specific version of containerhelper moved to avoid many
warnings
- Add scenarios for PowerPlatform
- Add PowerPlatformSolution artifact to builds
- Add unpack parameter to DownloadArtifact to unpack after download.

TO:DO
- [x] Fix failing CI tests
- [x] Ensure End 2 End test are passing
- [x] Increment version number in PowerPlatform project (awaits
Increment Version Number PR from @mazhelez)
- [x] Unit Tests
- [x] End 2 End test
- [x] Remove PREVIEW prefix from various docs
- [x] Remove usage of private version of BcContainerHelper
- [x] Release notes
- [x] Document new settings

---------

Co-authored-by: freddydk <[email protected]>
Co-authored-by: Maria Zhelezova <[email protected]>
Co-authored-by: Alexander Holstrup <[email protected]>
Co-authored-by: andersgMSFT <[email protected]>
  • Loading branch information
5 people authored May 14, 2024
1 parent 654dd54 commit 7a8c4a7
Show file tree
Hide file tree
Showing 105 changed files with 3,879 additions and 376 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
run: |
. (Join-Path "." "Tests/runtests.ps1")
- name: Test AL-GO Workflows
- name: Test AL-Go Workflows
if: github.repository_owner == 'microsoft'
run: |
try {
Expand Down
34 changes: 21 additions & 13 deletions Actions/AL-Go-Helper.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ $defaultCICDPullRequestBranches = @( 'main' )
$runningLocal = $local.IsPresent
$defaultBcContainerHelperVersion = "preview" # Must be double quotes. Will be replaced by BcContainerHelperVersion if necessary in the deploy step - ex. "https://github.com/organization/navcontainerhelper/archive/refs/heads/branch.zip"
$microsoftTelemetryConnectionString = "InstrumentationKey=84bd9223-67d4-4378-8590-9e4a46023be2;IngestionEndpoint=https://westeurope-1.in.applicationinsights.azure.com/"
$notSecretProperties = @("Scopes","TenantId","BlobName","ContainerName","StorageAccountName","ServerUrl")
$notSecretProperties = @("Scopes","TenantId","BlobName","ContainerName","StorageAccountName","ServerUrl","ppUserName")

$runAlPipelineOverrides = @(
"DockerPull"
Expand Down Expand Up @@ -554,6 +554,7 @@ function ReadSettings {
"type" = "PTE"
"unusedALGoSystemFiles" = @()
"projects" = @()
"powerPlatformSolutionFolder" = ""
"country" = "us"
"artifact" = ""
"companyName" = ""
Expand Down Expand Up @@ -2301,12 +2302,30 @@ function RetryCommand {
}
}

function GetMatchingProjects {
Param(
[string[]] $projects,
[string] $selectProjects = ''
)

if ($selectProjects) {
# Filter the project list based on the projects parameter
if ($selectProjects.StartsWith('[')) {
$selectProjects = ($selectProjects | ConvertFrom-Json) -join ","
}
$projectArr = $selectProjects.Split(',').Trim()
$projects = @($projects | Where-Object { $project = $_; if ($projectArr | Where-Object { $project -like $_ }) { $project } })
}
return $projects
}

function GetProjectsFromRepository {
Param(
[string] $baseFolder,
[string[]] $projectsFromSettings,
[string] $selectProjects = ''
)

if ($projectsFromSettings) {
$projects = $projectsFromSettings
}
Expand All @@ -2318,18 +2337,7 @@ function GetProjectsFromRepository {
$projects += @(".")
}
}
if ($selectProjects) {
# Filter the project list based on the projects parameter
if ($selectProjects.StartsWith('[')) {
$selectProjects = ($selectProjects | ConvertFrom-Json) -join ","
}
$projectArr = $selectProjects.Split(',').Trim()
$projects = @($projects | Where-Object { $project = $_; if ($projectArr | Where-Object { $project -like $_ }) { $project } })
if ($projects.Count -eq 0) {
throw "No projects matches '$selectProjects'"
}
}
return $projects
return @(GetMatchingProjects -projects $projects -selectProjects $selectProjects)
}

function Get-PackageVersion($PackageName) {
Expand Down
7 changes: 7 additions & 0 deletions Actions/AL-Go-TestRepoHelper.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,20 @@ function Test-SettingsJson {
Test-Shell -json $json -settingsDescription $settingsDescription -property 'shell'
Test-Shell -json $json -settingsDescription $settingsDescription -property 'gitHubRunnerShell'

if ($json.Keys -contains 'bcContainerHelperVersion') {
if ($json.bcContainerHelperVersion -ne 'latest' -and $json.bcContainerHelperVersion -ne 'preview') {
OutputWarning -Message "Using a specific version of BcContainerHelper in $settingsDescription is not recommended and will lead to build failures in the future. Consider removing the setting."
}
}

if ($type -eq 'Repo') {
# Test for things that should / should not exist in a repo settings file
Test-Property -settingsDescription $settingsDescription -json $json -key 'templateUrl' -should
}
if ($type -eq 'Project') {
# GitHubRunner should not be in a project settings file (only read from repo or workflow settings)
Test-Property -settingsDescription $settingsDescription -json $json -key 'githubRunner' -shouldnot
Test-Property -settingsDescription $settingsDescription -json $json -key 'bcContainerHelperVersion' -shouldnot
}
if ($type -eq 'Workflow') {
# Test for things that should / should not exist in a workflow settings file
Expand Down
274 changes: 274 additions & 0 deletions Actions/BuildPowerPlatform/BuildPowerPlatform.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
[CmdletBinding()]
param(
[Parameter(mandatory = $true)]
[string] $solutionFolder,
[Parameter(mandatory = $false)]
[string] $companyId,
[Parameter(mandatory = $false)]
[string] $environmentName,
[Parameter(mandatory = $false)]
[string] $appBuild,
[Parameter(mandatory = $false)]
[string] $appRevision
)
$ErrorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0

function Update-PowerAppSettings {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string] $SolutionFolder,
[Parameter(Mandatory = $true)]
[string] $environmentName,
[Parameter(Mandatory = $true)]
[string] $companyId
)

# There are multiple files that contain the BC connection info for PowerApps with different structures
# So instead of parsing all of them, we simple find the current connection strings and run a replace operation.
# Note: The connection string has a format of: "EnvironmentName,CompanyId" where companyId is a guid. So the
# replace operation should be safe to run a all json and XML files.
Write-Host "Updating PowerApp settings"
$currentPowerAppSettings = Get-CurrentPowerAppSettings -solutionFolder $SolutionFolder
if ($null -eq $currentPowerAppSettings) {
Write-Host "::Notice::No Power Apps found"
return
}

Write-Host "Number of Business Central Power App connections found: "$currentPowerAppSettings.Count
$newSettings = "$environmentName,$companyId"
foreach ($currentSetting in $currentPowerAppSettings) {
if ($currentSetting -eq $newSettings) {
Write-Host "No changes needed for: "$currentSetting
continue
}
Update-PowerAppFiles -oldSetting $currentSetting -newSetting $newSettings -solutionFolder $SolutionFolder
}
}

function Update-PowerAppFiles {
param(
[Parameter(mandatory = $true)]
[string] $solutionFolder,
[Parameter(mandatory = $true)]
[string] $oldSetting,
[Parameter(mandatory = $true)]
[string] $newSetting
)

$powerAppFiles = Get-ChildItem -Recurse -File "$solutionFolder/CanvasApps"
foreach ($file in $powerAppFiles) {
# only check json and xml files
if (($file.Extension -eq ".json") -or ($file.Extension -eq ".xml")) {
$fileContent = Get-Content $file.FullName
if (Select-String -Pattern $oldSetting -InputObject $fileContent) {
$fileContent = $fileContent -creplace $oldSetting, $newSetting
Set-Content -Path $file.FullName -Value $fileContent
Write-Host "Updated: $($file.FullName)"
}
}
}
}

function Get-CurrentPowerAppSettings {
param (
[Parameter(mandatory = $true)]
[string] $solutionFolder
)

if (-not (Test-Path -Path "$solutionFolder/CanvasApps")) {
# No Canvas apps present in the solution
return @()
}

$currentSettingsList = @()
$connectionsFilePaths = Get-ChildItem -Path "$solutionFolder/CanvasApps" -Recurse -File -Include "Connections.json" | Select-Object -ExpandProperty FullName
foreach ($connectionsFilePath in $connectionsFilePaths) {
$jsonFile = Get-Content $connectionsFilePath | ConvertFrom-Json

# We don't know the name of the connector node, so we need to loop through all of them
$ConnectorNodeNames = ($jsonFile | Get-Member -MemberType NoteProperty).Name

foreach ($connectorNodeName in $ConnectorNodeNames) {
$connectorNode = $jsonFile.$connectorNodeName
# Find the Business Central connection node
if ($connectorNode.connectionRef.displayName -eq "Dynamics 365 Business Central") {
$currentEnvironmentAndCompany = ($connectorNode.datasets | Get-Member -MemberType NoteProperty).Name

if ($null -eq $currentEnvironmentAndCompany) {
# Connections sections for Power Automate flow does not have a dataset node
# Note: Flows are handled in a different function
continue
}

if (!$currentSettingsList.Contains($currentEnvironmentAndCompany)) {
$currentSettingsList += $currentEnvironmentAndCompany

# The Business Central environment can be inconsistent - Either starting with a capital letter or all caps.
# Add both variants to ensure we find all connections
$currentSettingsParts = @($currentEnvironmentAndCompany.Split(","))
$currentSettingsList += "$($currentSettingsParts[0].ToUpperInvariant()),$($currentSettingsParts[1])"
}
}
}
}
return $currentSettingsList
}

function Update-FlowSettings {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string] $SolutionFolder,
[Parameter(Mandatory = $true)]
[string] $environmentName,
[Parameter(Mandatory = $true)]
[string] $companyId
)

Write-Host "Updating Flow settings"
$flowFilePaths = Get-ChildItem -Path (Join-Path $SolutionFolder 'Workflows') -Recurse -Filter '*.json' | Select-Object -ExpandProperty FullName

if ($null -eq $flowFilePaths) {
Write-Host "::Notice::No Power Automate flows found"
return
}

foreach ($flowFilePath in $flowFilePaths) {
Update-FlowFile -FilePath $flowFilePath -CompanyId $companyId -EnvironmentName $environmentName
}
}

function Update-FlowFile {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string] $filePath,
[Parameter(Mandatory = $true)]
[string] $companyId,
[Parameter(Mandatory = $true)]
[string] $environmentName
)
# Read the JSON file
$jsonObject = Get-Content $filePath | ConvertFrom-Json
$shouldUpdate = $false

# Update all flow triggers
$triggersObject = $jsonObject.properties.definition.triggers
$triggers = $triggersObject | Get-Member -MemberType Properties
foreach ($trigger in $triggers) {
$triggerInputs = $triggersObject.($trigger.Name).inputs

if ($triggerInputs | Get-Member -MemberType Properties -name 'parameters') {
# Business Central triggers have connection information in the input parameters
if (Update-ParameterObject -parametersObject $triggerInputs.parameters -CompanyId $companyId -EnvironmentName $environmentName) {
$shouldUpdate = $true
}
}
}

# Update all flow actions
$actionsObject = $jsonObject.properties.definition.actions
$actions = $actionsObject | Get-Member -MemberType Properties
foreach ($action in $actions) {
$actionInput = $actionsObject.($action.Name).inputs
if ($actionInput | Get-Member -MemberType Properties -name 'parameters') {
# Business Central actions have connection information in the input parameters
if (Update-ParameterObject -parametersObject $actionInput.parameters -CompanyId $companyId -EnvironmentName $environmentName) {
$shouldUpdate = $true
}
}
}
if ($shouldUpdate) {
# Save the updated JSON back to the file
$jsonObject | ConvertTo-Json -Depth 100 | Set-Content $filePath
Write-Host "Updated: $filePath"
}
else {
Write-Host "No update needed for: $filePath"
}
}

function Update-ParameterObject {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[System.Object] $parametersObject,
[Parameter(Mandatory = $true)]
[string] $companyId,
[Parameter(Mandatory = $true)]
[string] $environmentName
)
# Check if paramers are for Business Central
if ((-not $parametersObject.company) -or (-not $parametersObject.bcEnvironment)) {
return $false
}

$oldCompany = $parametersObject.company
$oldBcEnvironment = $parametersObject.bcenvironment

# Check if parameters are already set to the correct values
if (($oldCompany -eq $companyId) -and ($oldBcEnvironment -eq $environmentName)) {
return $false
}

$enviromentVariablePlaceHolder = "@parameters("

# Check if parameters are set using a different approach (e.g. environment variables or passed in parameters)
if ($oldCompany.contains($enviromentVariablePlaceHolder) -or $oldBcEnvironment.contains($enviromentVariablePlaceHolder)) {
return $false
}

$parametersObject.company = $companyId
$parametersObject.bcEnvironment = $environmentName

return $true
}

function Update-SolutionVersionNode {
param(
[Parameter(mandatory = $true)]
[string] $appBuild,
[Parameter(mandatory = $true)]
[string] $appRevision,
[Parameter(mandatory = $true)]
[xml] $xmlFile
)

if ($appBuild -and $appRevision) {
$versionNode = $xmlFile.SelectSingleNode("//Version")
$versionNodeText = $versionNode.'#text'

$versionParts = $versionNodeText.Split('.')
# Only update the last two parts of the version number - major and minor version should be set manually
$newVersionNumber = $versionParts[0] + '.' + $versionParts[1] + '.' + $appBuild + '.' + $appRevision

Write-Host "New version: "$newVersionNumber
$versionNode.'#text' = $newVersionNumber
}

}

if ($appBuild -and $appRevision) {
Write-Host "Updating Power Platform solution file ($solutionFolder)"
$solutionDefinitionFile = Join-Path $solutionFolder 'other/Solution.xml'
if (-not (Test-Path -Path $solutionDefinitionFile)) {
throw "Solution file not found: $solutionDefinitionFile"
}
$xmlFile = [xml](Get-Content -Encoding UTF8 -Path $solutionDefinitionFile)
Update-SolutionVersionNode -appBuild $appBuild -appRevision $appRevision -xmlFile $xmlFile
$xmlFile.Save($solutionDefinitionFile)
}
else {
Write-Host "Skip solution version update since appBuild and appRevision are not set"
}

if ($environmentName -and $companyId) {
Write-Host "Updating the Power Platform solution Business Central connection settings"
Write-Host "New connections settings: $environmentName, $companyId"
Update-PowerAppSettings -SolutionFolder $SolutionFolder -EnvironmentName $environmentName -CompanyId $companyId
Update-FlowSettings -SolutionFolder $SolutionFolder -EnvironmentName $environmentName -CompanyId $companyId
}
else {
Write-Host "Skip Business Central connection settings update since EnvironmentName and CompanyId are not set"
}
22 changes: 22 additions & 0 deletions Actions/BuildPowerPlatform/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Build Power Platform
Build the Power Platform solution

## INPUT

### ENV variables
none

### Parameters
| Name | Required | Description | Default value |
| :-- | :-: | :-- | :-- |
| shell | | The shell (powershell or pwsh) in which the PowerShell script in this action should run | powershell |
| solutionFolder | Yes | The Power Platform solution path | |
| outputFolder | Yes | Output folder where the zip file will be placed | |
| outputFileName | Yes | The name of the output zip file | |
| companyId | | The Business Central company ID | |
| environmentName | | The Business Central environment name | |
| appBuild | | The app build number | |
| appRevision | | The app revision number | |

## OUTPUT
none
Loading

0 comments on commit 7a8c4a7

Please sign in to comment.