Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
.DESCRIPTION
This script reads *_Workflow.json files from a specified directory and creates/updates
workflows in an Azure Logic App Standard resource using the Azure REST API (via az CLI).
It also optionally deploys a parameters.json file for workflow parameters.
It also deploys parameters.json, deriving LogicAppVersion from VERSION in WorkflowsPath.
The source parameters file is not modified. VERSION must come from the same source bundle.

The script performs pre-deployment validation of all files before making any API calls,
ensuring no partial deployments occur if any file is invalid.
Expand All @@ -26,6 +27,10 @@
(Optional) Path to a parameters JSON file to deploy as the app-level parameters.json.
If not specified, the script will look for *_Parameters.json in the WorkflowsPath.

.PARAMETER SkipParameters
Retained for command-line compatibility, but rejected for versioned bundles.
Deploy parameters with workflows so final results cannot report a stale version.

.EXAMPLE
.\Deploy-LogicAppWorkflows.ps1 -SubscriptionId "xxxx" -ResourceGroup "myRG" -LogicAppName "myLogicApp"

Expand All @@ -34,7 +39,7 @@

.EXAMPLE
.\Deploy-LogicAppWorkflows.ps1 -SubscriptionId "xxxx" -ResourceGroup "myRG" -LogicAppName "myLogicApp" -SkipParameters
Deploys only workflow files without updating the parameters.json.
Rejected for versioned bundles: parameters must be deployed with the workflow version.

.NOTES
Prerequisites:
Expand Down Expand Up @@ -268,7 +273,8 @@ function Deploy-VfsFile {
[string]$RG,
[string]$AppName,
[string]$RemotePath,
[string]$LocalFilePath
[string]$LocalFilePath,
[string]$Content
)

# ARM-proxied VFS endpoint — routes through management.azure.com instead of SCM directly
Expand All @@ -284,7 +290,7 @@ function Deploy-VfsFile {
}

# Read the raw file content — preserves exact bytes including Logic Apps expressions
$bodyContent = Get-Content -Path $LocalFilePath -Raw -Encoding UTF8
$bodyContent = if ($PSBoundParameters.ContainsKey('Content')) { $Content } else { Get-Content -Path $LocalFilePath -Raw -Encoding UTF8 }

# Use Invoke-RestMethod to send raw content without JSON re-serialization
$headers = @{
Expand All @@ -295,6 +301,13 @@ function Deploy-VfsFile {

$result = Invoke-RestMethod -Method PUT -Uri $vfsUri -Headers $headers -Body $bodyContent -ContentType "application/json"

if ($RemotePath -eq "parameters.json") {
$readback = Invoke-RestMethod -Method GET -Uri $vfsUri -Headers $headers
if ($readback.LogicAppVersion.value -cne $script:BundleVersion) {
throw "Deployed LogicAppVersion does not match VERSION. Do not start the workflows."
}
}

return $true
}
catch {
Expand Down Expand Up @@ -351,6 +364,23 @@ function Deploy-Workflow {
}
}

function Get-VersionedParametersContent {
param(
[string]$ParametersFilePath,
[string]$BundleVersion
)

$parameters = Get-Content -LiteralPath $ParametersFilePath -Raw -Encoding UTF8 | ConvertFrom-Json
if ($null -eq $parameters -or $parameters.LogicAppVersion.type -cne 'String') {
throw "Parameters must define LogicAppVersion with type String."
}
if ([string]::IsNullOrWhiteSpace($BundleVersion)) {
throw "VERSION must contain a nonempty bundle version."
}
$parameters.LogicAppVersion | Add-Member -NotePropertyName value -NotePropertyValue $BundleVersion.Trim() -Force
return $parameters | ConvertTo-Json -Depth 100
}

function Deploy-Parameters {
<#
.SYNOPSIS
Expand All @@ -366,7 +396,7 @@ function Deploy-Parameters {
Write-LogMessage "Deploying parameters file: $ParametersFilePath" -Level Info

try {
$paramsContent = Get-Content -Path $ParametersFilePath -Raw -Encoding UTF8
$paramsContent = Get-VersionedParametersContent -ParametersFilePath $ParametersFilePath -BundleVersion $script:BundleVersion
$paramsObj = $paramsContent | ConvertFrom-Json

# Validate it's a valid JSON object
Expand All @@ -382,9 +412,9 @@ function Deploy-Parameters {

try {
# Upload parameters.json to root via ARM VFS
Deploy-VfsFile -SubId $SubId -RG $RG -AppName $AppName `
$null = Deploy-VfsFile -SubId $SubId -RG $RG -AppName $AppName `
-RemotePath "parameters.json" `
-LocalFilePath $ParametersFilePath
-Content $paramsContent

Write-LogMessage " Parameters file deployed successfully." -Level Success
return $true
Expand Down Expand Up @@ -412,6 +442,23 @@ if (-not $WorkflowsPath) {
exit 1
}

try {
if ($SkipParameters) {
throw "-SkipParameters is not supported for versioned bundles. Deploy parameters with the workflows to avoid stale version metadata."
}
if ($ParametersFile -and -not (Test-Path -LiteralPath $ParametersFile -PathType Leaf)) {
throw "Parameters file does not exist: $ParametersFile"
}
$script:BundleVersion = (Get-Content -LiteralPath (Join-Path $WorkflowsPath 'VERSION') -Raw -Encoding UTF8).Trim()
if ([string]::IsNullOrWhiteSpace($script:BundleVersion)) {
throw "VERSION must contain a nonempty bundle version."
}
}
catch {
Write-LogMessage "Version preparation failed: $_" -Level Error
exit 1
}

Write-LogMessage "Configuration:" -Level Info
Write-Host " Subscription ID : $SubscriptionId"
Write-Host " Resource Group : $ResourceGroup"
Expand Down Expand Up @@ -482,6 +529,12 @@ if ($SkipParameters) {
}
elseif ($ParametersFile -and (Test-Path $ParametersFile)) {
$pErrors = Test-ParametersFile -FilePath $ParametersFile -FileName (Split-Path $ParametersFile -Leaf)
try {
$null = Get-VersionedParametersContent -ParametersFilePath $ParametersFile -BundleVersion $script:BundleVersion
}
catch {
$pErrors += "Version preparation failed: $_"
}
if ($pErrors.Count -gt 0) {
$validationFailed = $true
foreach ($err in $pErrors) {
Expand All @@ -494,8 +547,18 @@ elseif ($ParametersFile -and (Test-Path $ParametersFile)) {
}
else {
$autoParamFiles = Get-ParametersFiles -Path $WorkflowsPath
if ($autoParamFiles.Count -eq 0) {
Write-LogMessage "A parameters file is required to deploy the bundle version." -Level Error
$validationFailed = $true
}
foreach ($pf in $autoParamFiles) {
$pErrors = Test-ParametersFile -FilePath $pf.FullName -FileName $pf.Name
try {
$null = Get-VersionedParametersContent -ParametersFilePath $pf.FullName -BundleVersion $script:BundleVersion
}
catch {
$pErrors += "Version preparation failed: $_"
}
if ($pErrors.Count -gt 0) {
$validationFailed = $true
foreach ($err in $pErrors) {
Expand Down Expand Up @@ -535,12 +598,20 @@ else {
foreach ($pf in $paramFiles) {
Write-LogMessage " Found: $($pf.Name)" -Level Info
$paramsDeployed = Deploy-Parameters -SubId $SubscriptionId -RG $ResourceGroup -AppName $LogicAppName -ParametersFilePath $pf.FullName
if (-not $paramsDeployed) {
Write-LogMessage "Parameters deployment or version read-back failed. No workflows will be uploaded." -Level Error
exit 1
}
}
}
else {
Write-LogMessage "Step 6: No parameters file found. Skipping parameters deployment." -Level Info
}
}
if (-not $paramsDeployed) {
Write-LogMessage "Parameters deployment or version read-back failed. No workflows will be uploaded." -Level Error
exit 1
}
Write-Host ""

# --- Step 7: Deploy workflows ---
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
{
"selectedAuthenticationMethod": {
"type": "String",
"value": "",
"metadata": {
"description": "ISV-selected authentication test for this run: OAuth2ClientCredentials or FederatedIdentity. Direct semantic tests still use scimBearerToken."
}
},
"EnabledTests": {
"allowedValues": [
"All",
Expand Down Expand Up @@ -36,10 +43,10 @@
},
"LogicAppVersion": {
"metadata": {
"description": "Version of the Logic App for tracking and deployment purposes"
"description": "Populated during deployment from VERSION in the same source bundle."
},
"type": "String",
"value": "10.0"
"value": ""
},
"attributeTransforms": {
"metadata": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"workflowId": "@workflow().id",
"workflowName": "@workflow().name",
"workflowRunName": "@workflow().run.name",
"validationStartedAtUtc": "@utcNow()",
"workflowIdSplit": "@split(workflow().id, '/')",
"workflowIdSplitLength": "@length(split(workflow().id, '/'))"
}
Expand Down Expand Up @@ -504,9 +505,10 @@
"GroupTests_Workflow": "@outputs('Compose_GroupTests_RunLink')",
"SCIMTests_Workflow": "@outputs('Compose_SCIMTests_RunLink')"
},
"overallLogicAppResult": "@if(or(not(equals(coalesce(body('Get_UserTests_Output_Content')?['CreateUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_UserTests_Output_Content')?['UpdateUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_UserTests_Output_Content')?['DisableUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isManagerAttributeSupported'],false),not(equals(coalesce(body('Get_UserTests_Output_Content')?['ManagerTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_UserTests_Output_Content')?['RestoreUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMNullUpdateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['CreateGroupTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['UpdateGroupTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['AddMemberTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['RemoveMemberTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SchemaDiscoverabilityTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserCreateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupCreateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserUpdateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupUpdateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserPaginationTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_UserTests_Output_Content')?['PODUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['PODGroupTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),equals(actions('Call_Initialization_Workflow')?['status'],'Failed'),equals(coalesce(body('Poll_UserTests_Status')?['properties']?['status'],''),'Failed'),equals(coalesce(body('Poll_GroupTests_Status')?['properties']?['status'],''),'Failed'),or(equals(coalesce(body('Get_SCIMTests_Output_Content')?['authenticationModelResult'],'FAILED'),'FAILED'),and(coalesce(body('Call_Initialization_Workflow')?['isManagerAttributeSupported'],false),contains(string(coalesce(body('Call_Initialization_Workflow')?['targetDirectoryUserAttributes'],json('[]'))),'manager'),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUpdateManagerTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))))),'Failed',if(equals(coalesce(body('Poll_SCIMTests_Status')?['properties']?['status'],''),'Failed'),'Failed','Succeeded'))",
"overallLogicAppResult": "@if(or(not(equals(coalesce(body('Get_UserTests_Output_Content')?['CreateUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_UserTests_Output_Content')?['UpdateUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_UserTests_Output_Content')?['DisableUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isManagerAttributeSupported'],false),not(equals(coalesce(body('Get_UserTests_Output_Content')?['ManagerTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_UserTests_Output_Content')?['RestoreUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMNullUpdateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['CreateGroupTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['UpdateGroupTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['AddMemberTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['RemoveMemberTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SchemaDiscoverabilityTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserCreateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupCreateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserUpdateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupUpdateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserPaginationTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_UserTests_Output_Content')?['PODUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['PODGroupTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),equals(actions('Call_Initialization_Workflow')?['status'],'Failed'),equals(coalesce(body('Poll_UserTests_Status')?['properties']?['status'],''),'Failed'),equals(coalesce(body('Poll_GroupTests_Status')?['properties']?['status'],''),'Failed'),or(not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['authenticationModelResult'],'FAILED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isManagerAttributeSupported'],false),contains(string(coalesce(body('Call_Initialization_Workflow')?['targetDirectoryUserAttributes'],json('[]'))),'manager'),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUpdateManagerTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))))),'Failed',if(equals(coalesce(body('Poll_SCIMTests_Status')?['properties']?['status'],''),'Failed'),'Failed','Succeeded'))",
"optionalSuccessTestsResult": "@if(or(not(equals(coalesce(body('Get_UserTests_Output_Content')?['DeleteUserTestOutputs']?['overallResult'], 'SKIPPED'), 'PASSED')), and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'], false), not(equals(coalesce(body('Get_GroupTests_Output_Content')?['DeleteGroupTestOutputs']?['overallResult'], 'SKIPPED'), 'PASSED'))), and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'], false), not(equals(coalesce(body('Get_GroupTests_Output_Content')?['RestoreGroupTestOutputs']?['overallResult'], 'SKIPPED'), 'PASSED'))), and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'], false), not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupPaginationTestOutputs']?['overallResult'], 'SKIPPED'), 'PASSED')))), 'WARNING', 'PASSED')",
"authenticationModelResult": "@coalesce(body('Get_SCIMTests_Output_Content')?['authenticationModelResult'], 'FAILED')"
"authenticationModelResult": "@coalesce(body('Get_SCIMTests_Output_Content')?['authenticationModelResult'], 'FAILED')",
"validationCompletedAtUtc": "@utcNow()"
}
},
"Final_TestResults": {
Expand Down Expand Up @@ -751,7 +753,14 @@
"SCIMTests_Workflow": "@outputs('Compose_SCIMTests_RunLink')"
},
"authenticationModelResult": "@outputs('Evaluate_Test_Results')?['authenticationModelResult']",
"overallResult": "@outputs('Evaluate_Test_Results')?['overallLogicAppResult']"
"overallResult": "@outputs('Evaluate_Test_Results')?['overallLogicAppResult']",
"selectedAuthenticationMethod": "@parameters('selectedAuthenticationMethod')",
"LogicAppVersion": "@parameters('LogicAppVersion')",
"validationStartedAtUtc": "@outputs('Compose_Debug_WorkflowObject')?['validationStartedAtUtc']",
"validationStartedAtEpochSeconds": "@div(sub(ticks(outputs('Compose_Debug_WorkflowObject')?['validationStartedAtUtc']), ticks('1970-01-01T00:00:00Z')), 10000000)",
"validationCompletedAtUtc": "@outputs('Evaluate_Test_Results')?['validationCompletedAtUtc']",
"validationCompletedAtEpochSeconds": "@div(sub(ticks(outputs('Evaluate_Test_Results')?['validationCompletedAtUtc']), ticks('1970-01-01T00:00:00Z')), 10000000)",
"validationDurationSeconds": "@div(sub(ticks(outputs('Evaluate_Test_Results')?['validationCompletedAtUtc']), ticks(outputs('Compose_Debug_WorkflowObject')?['validationStartedAtUtc'])), 10000000)"
}
},
"Terminate_With_Test_Status": {
Expand Down
Loading
Loading