parameters: - name: WORKSPACE_NAME displayName: Workspace name to conduct tests? type: string default: '' trigger: branches: include: - main pool: vmimage: 'windows-latest' variables: # Variable group with configuration - group: TestingCredentialsLogShipping - name: 'WORKSPACE_NAME' value: '${{ parameters.WORKSPACE_NAME }}' jobs: - job: Job_1 displayName: Automated Deployment and Testing Job steps: - checkout: self fetchDepth: 0 - task: PowerShell@2 displayName: Install Dependencies inputs: pwsh: true targetType: inline script: | # ---------- Check if PowerShell Modules are Installed ---------- # # Install Az.Accounts if Needed if (!(Get-Module -ListAvailable -Name "Az.Accounts")) { #Install Az.Accounts Module Install-Module -Name Az.Accounts -Scope CurrentUser -AllowClobber -Force } # Install Invoke-DQVTesting #Install-Module -Name Invoke-DQVTesting -Scope CurrentUser -AllowClobber -Force Install-Module -Name Invoke-DQVTesting -Scope CurrentUser -AllowClobber -Force -AllowPrerelease # Install Invoke-SemanticModelRefresh Install-Module -Name Invoke-SemanticModelRefresh -Scope CurrentUser -AllowClobber -Force # Create a new directory in the current location if((Test-Path -path ".\.nuget\custom_modules") -eq $false){ New-Item -Name ".nuget\custom_modules" -Type Directory } # For each url download and install in module folder @("https://raw.githubusercontent.com/microsoft/Analysis-Services/master/pbidevmode/fabricps-pbip/FabricPS-PBIP.psm1", "https://raw.githubusercontent.com/microsoft/Analysis-Services/master/pbidevmode/fabricps-pbip/FabricPS-PBIP.psd1") | ForEach-Object { Invoke-WebRequest -Uri $_ -OutFile ".\.nuget\custom_modules\$(Split-Path $_ -Leaf)" } - task: PowerShell@2 displayName: Deploy Changes for Semantic Models, Reports and Conduct Testing For Semantic Models env: CLIENT_SECRET: $(CLIENT_SECRET) # Maps the secret variable inputs: pwsh: true failOnStderr: true targetType: inline script: | # ---------- Import PowerShell Modules ---------- # # Import FabricPS-PBIP Import-Module ".\.nuget\custom_modules\FabricPS-PBIP" -Force # ---------- Setup Credentials ---------- # $secret = ${env:CLIENT_SECRET} | ConvertTo-SecureString -AsPlainText -Force $credential = [System.Management.Automation.PSCredential]::new(${env:CLIENT_ID},$secret) # Check if service principal or username/password $guidRegex = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' $isServicePrincipal = $false if($credential.UserName -match $guidRegex){# Service principal used $isServicePrincipal = $true } # ---------- Login to Azure Copy and Fabric ---------- # # Convert secure string to plain text to use in connection strings $secureStringPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($credential.Password) $plainTextPwd = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($secureStringPtr) # Set Fabric Connection if($isServicePrincipal){ Set-FabricAuthToken -servicePrincipalId $credential.UserName ` -servicePrincipalSecret $plainTextPwd ` -tenantId ${env:TENANT_ID} -reset }else{ # User account Set-FabricAuthToken -credential $credential -tenantId ${env:TENANT_ID} -reset } # Set AzCopy Connection $env:AZCOPY_SPA_CLIENT_SECRET = $plainTextPwd $onelakeUri = New-Object System.Uri("${env:ONELAKE_ENDPOINT}" ) $onelakeDomain = $onelakeUri.Host $loginResult = azcopy login --service-principal ` --application-id $credential.UserName ` --tenant-id "${env:TENANT_ID}" ` --trusted-microsoft-suffixes="$($onelakeDomain)" ` --output-type json | ConvertFrom-Json # Check if login was successful $checkResult = $loginResult | Where-Object {$_.MessageContent -eq "INFO: SPN Auth via secret succeeded."} if(!$checkResult) { Write-Host "##[error] Failed to login to azcopy" } # ---------- Identify Changes For Promotion ---------- # $pbipSMChanges = @(git diff --name-only --relative --diff-filter=d HEAD~1..HEAD -- '*.Dataset/**' '*.SemanticModel/**') $pbipSMChanges += @(git diff --name-only --relative --diff-filter=d HEAD~2..HEAD -- '*.Dataset/**' '*.SemanticModel/**') $pbipSMChanges = $pbipSMChanges | Sort-Object -Unique $pbipRptChanges = @(git diff --name-only --relative --diff-filter=d HEAD~1..HEAD -- '*.Report/**') $pbipRptChanges += @(git diff --name-only --relative --diff-filter=d HEAD~2..HEAD -- '*.Report/**') $pbipRptChanges = $pbipRptChanges | Sort-Object -Unique # Detect if no changes if($pbipSMChanges -eq $null -and $pbipRptChanges -eq $null){ Write-Host "No changes detected in the Semantic Model or Report folders. Exiting..." exit 0 } # Get workspace Id $workspaceObj = Get-FabricWorkspace -workspaceName "${env:WORKSPACE_NAME}" $workspaceID = $workspaceObj.Id # ---------- Handle Semantic Models For Promotion ---------- # # Identify Semantic Models changed $sMPathsToPromote = @() $filter = "*.pbism" foreach($change in $pbipSMChanges){ $parentFolder = Split-Path $change -Parent while ($null -ne $parentFolder -and !( Test-Path( Join-Path $parentFolder $filter))) { $parentFolder = Split-Path $parentFolder -Parent } $sMPathsToPromote += $parentFolder }# end foreach # Remove duplicates $sMPathsToPromote = @([System.Collections.Generic.HashSet[string]]$sMPathsToPromote) # Setup promoted items array $sMPromotedItems= @() # Promote semantic models to workspace foreach($promotePath in $sMPathsToPromote){ Write-Host "##[debug]Promoting semantic model at $($promotePath) to workspace ${env:WORKSPACE_NAME}" $sMPromotedItems += Import-FabricItem -workspaceId $workspaceID -path $promotePath }# end foreach # ---------- Promote Reports ---------- # # Retrieve all items in workspace # Do this after semantic models have been promoted $items = Invoke-FabricAPIRequest -Uri "workspaces/$($workspaceID)/items" -Method Get # Identify Reports changed $rptPathsToPromote = @() $filter = "*.pbir" foreach($change in $pbipRptChanges){ $parentFolder = Split-Path $change -Parent while ($null -ne $parentFolder -and !( Test-Path( Join-Path $parentFolder $filter))) { $parentFolder = Split-Path $parentFolder -Parent } $rptPathsToPromote += $parentFolder }# end foreach # Remove duplicates $rptPathsToPromote = @([System.Collections.Generic.HashSet[string]]$rptPathsToPromote) # Setup promoted items array $rptPromotedItems= @() # Promote reports to workspace foreach($promotePath in $rptPathsToPromote){ # Get report definition $def = Get-ChildItem -Path $promotePath -Recurse -Include "definition.pbir" $semanticModelPath = (Get-Content $def.FullName | ConvertFrom-Json).datasetReference.byPath # If byPath was null, we'll assume byConnection is set and skip if($semanticModelPath -ne $null){ # Semantic Model path is relative to the report path, Join-Path can handle relative paths $pathToCheck = Join-Path $promotePath $semanticModelPath.path $metadataSM = Get-ChildItem -Path $pathToCheck -Recurse -Include "item.metadata.json",".platform" | ` Where-Object {(Split-Path -Path $_.FullName).EndsWith(".Dataset") -or (Split-Path -Path $_.FullName).EndsWith(".SemanticModel")} $semanticModelName = $null $semanticModel = $null # If file is there let's get the display name if($metadataSM -ne $null){ $content = Get-Content $metadataSM.FullName | ConvertFrom-Json # Handle item.metadata.json if($metadataSM.Name -eq 'item.metadata.json'){ # prior to March 2024 release $semanticModelName = $content.displayName }else{ $semanticModelName = $content.metadata.displayName } #end if } else{ Write-Host "##[vso[task.logissue type=error]Semantic Model definition not found in workspace." } # Get the semantic model id from items in the workspace $semanticModel = $items | Where-Object {$_.type -eq "SemanticModel" -and $_.displayName -eq $semanticModelName} if(!$semanticModel){ Write-Host "##[vso[task.logissue type=error]Semantic Model not found in workspace." }else{ # Import report with appropriate semantic model id Write-Host "##[debug]Promoting report at $($promotePath) to workspace ${env:WORKSPACE_NAME}" $promotedItem = Import-FabricItem -workspaceId $workspaceID -path $promotePath -itemProperties @{semanticmodelId = "$($semanticModel.id)"} $rptPromotedItems += $promotedItem } }else{ # Promote thin report that already has byConnection set Write-Host "##[debug]Promoting report at $($promotePath) to workspace ${env:WORKSPACE_NAME}" $promotedItem = Import-FabricItem -workspaceId $workspaceID -path $promotePath $rptPromotedItems += $promotedItem } }# end foreach # ---------- Run Refreshes and Tests ---------- # # Generate Run-GUID $runGuid = (New-Guid).Guid $projectName = $($env:SYSTEM_TEAMPROJECT) $repoName = $($env:BUILD_REPOSITORY_NAME) $branchName = $($env:BUILD_SOURCEBRANCHNAME) $userName = "$($env:BUILD_REQUESTEDFOREMAIL)" $buildReason = "$($env:BUILD_REASON)" if($buildReason -eq 'Schedule' -or $buildReason -eq 'ScheduleForced'){ $userName = "Scheduled - Build Agent" } Write-Host "##[debug]Run GUID: $($runGuid)" Write-Host "##[debug]Project Name: $($projectName)" Write-Host "##[debug]Repository Name: $($repoName)" Write-Host "##[debug]Branch Name: $($branchName)" Write-Host "##[debug]User Name: $($userName)" $iDQVersion = (Get-Module -Name "Invoke-DQVTesting" -ListAvailable).Version.toString() $testResults = @() # Get UTC representation of run $dateTimeofRun = (Get-Date -Format "yyyy-MM-ddTHH-mm-ssZ") $fileName = "$($dateTimeofRun)-$($runGuid).csv" # Run synchronous refresh for each semantic model foreach($promotedItem in $sMPromotedItems){ # Test refresh which validates functionality Invoke-SemanticModelRefresh -WorkspaceId $workspaceID ` -SemanticModelId $promotedItem.Id ` -Credential $credential ` -TenantId "${env:TENANT_ID}" ` -Environment Public ` -LogOutput "ADO" # Run tests for functionality and data accuracy $testResults = @() $testResults = Invoke-DQVTesting -WorkspaceName "${env:WORKSPACE_NAME}" ` -Credential $credential ` -TenantId "${env:TENANT_ID}" ` -DatasetId $promotedItem.Id ` -LogOutput "Table" # Add additional properties to the array of objects $i = 0 $testResults | ForEach-Object { Add-Member -InputObject $_ -Name "BranchName" -Value $branchName -MemberType NoteProperty Add-Member -InputObject $_ -Name "RepositoryName" -Value $repoName -MemberType NoteProperty Add-Member -InputObject $_ -Name "ProjectName" -Value $projectName -MemberType NoteProperty Add-Member -InputObject $_ -Name "UserName" -Value $userName -MemberType NoteProperty Add-Member -InputObject $_ -Name "RunID" -Value $runGuid -MemberType NoteProperty Add-Member -InputObject $_ -Name "Order" -Value $i -MemberType NoteProperty Add-Member -InputObject $_ -Name "RunDateTime" -Value $dateTimeofRun -MemberType NoteProperty Add-Member -InputObject $_ -Name "InvokeDQVTestingVersion" -Value $iDQVersion -MemberType NoteProperty $i++ } $testResults | Select-Object * | Export-Csv ".\$fileName" $getAbsPath = (Resolve-Path ".\$fileName").Path Write-Host "##[debug]Test Results for $($promotedItem.Id) saved locally to $($getAbsPath)." Write-Host "##[debug]Copying file to lakehouse at ${env:ONELAKE_ENDPOINT}." # Copy file to lakehouse $copyResults = azcopy copy $getAbsPath "${env:ONELAKE_ENDPOINT}" --overwrite=true ` --blob-type=Detect ` --check-length=true ` --put-md5 ` --trusted-microsoft-suffixes="$($onelakeDomain)" ` --output-type json | ConvertFrom-Json # Check if copy was successful $checkCopyResults = $copyResults | Where-Object {$_.MessageType -eq "EndOfJob"} if(!$checkCopyResults) { Write-Host "##[error]Failed to copy file to lakehouse" }else{ Write-Host "##[debug]Successfully copied file to lakehouse" } # Output test results $testResults | ForEach-Object { $prefix = "##[debug]" switch($_.LogType){ 'Warning' { $prefix = "##vso[task.logissue type=warning]"} 'Error' { $prefix = "##vso[task.logissue type=error]"} 'Failure' { $prefix = "##vso[task.complete result=Failed;]"} 'Success' { $prefix = "##vso[task.complete result=Succeeded;]"} } Write-Host "$($prefix)$($_.Message)" }# end foreach test results that is truly a test result }# end foreach