From e4cc9573670b80aee65fd098e825fbd20aaa25a8 Mon Sep 17 00:00:00 2001 From: Alexander Sehr Date: Sun, 17 Dec 2023 23:56:17 +0100 Subject: [PATCH] feat: Updated workflow file tests & small bugfix for utility (#707) ## Description - Refactored test cases and output for current workflow test - Added test case to ensure the `workflowPath` variable has the correct expected value - In the process added a generic function to get any environment variable defined in a given workflow - Small bugfixes to trigger for branch function & added example to ONLY get pipeline badges - Fixed bug in fallback logic of PSRule formatter ## Example outputs ### Valid file ``` Describing Pipeline tests [+] [operational-insights/workspace] Module should have a GitHub workflow in path [.github/workflows/avm.res.operational-insights.workspace.yml]. 24ms (18ms|6ms) [+] [operational-insights/workspace] GitHub workflow [avm.res.operational-insights.workspace.yml] should have [workflowPath] environment variable with value [.github/workflows/avm.res.operational-insights.workspace.yml]. 11ms (10ms|2ms) ``` ### Invalid file ``` Describing Pipeline tests [+] [operational-insights/workspace] Module should have a GitHub workflow in path [.github/workflows/avm.res.operational-insights.workspace.yml]. 9ms (3ms|6ms) [-] [operational-insights/workspace] GitHub workflow [avm.res.operational-insights.workspace.yml] should have [workflowPath] environment variable with value [.github/workflows/avm.res.operational-insights.workspace.yml]. 20ms (18ms|2ms) Expected strings to be the same, but they were different. Expected length: 60 Actual length: 56 Strings differ at index 56. Expected: '.github/workflows/avm.res.operational-insights.workspace.yml' But was: '.github/workflows/avm.res.operational-insights.workspace' --------------------------------------------------------^ at $environmentVariables['workflowPath'] | Should -Be ".github/workflows/$workflowFileName", C:\dev\ip\Azure-bicep-registry-modules\alexanderSehr-fork\avm\utilities\pipelines\staticValidation\compliance\module.tests.ps1:268 at , C:\dev\ip\Azure-bicep-registry-modules\alexanderSehr-fork\avm\utilities\pipelines\staticValidation\compliance\module.tests.ps1:268 ``` ## Pipeline reference | Pipeline | | - | [![avm.res.batch.batch-account](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.batch.batch-account.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.batch.batch-account.yml) [![avm.res.cognitive-services.account](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.cognitive-services.account.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.cognitive-services.account.yml) [![avm.res.compute.ssh-public-key](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.compute.ssh-public-key.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.compute.ssh-public-key.yml) [![avm.res.db-for-postgre-sql.flexible-server](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.db-for-postgre-sql.flexible-server.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.db-for-postgre-sql.flexible-server.yml) [![avm.res.document-db.database-account](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.document-db.database-account.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.document-db.database-account.yml) [![avm.res.event-grid.domain](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.event-grid.domain.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.event-grid.domain.yml) [![avm.res.event-grid.system-topic](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.event-grid.system-topic.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.event-grid.system-topic.yml) [![avm.res.event-grid.topic](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.event-grid.topic.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.event-grid.topic.yml) [![avm.res.insights.action-group](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.insights.action-group.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.insights.action-group.yml) [![avm.res.insights.component](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.insights.component.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.insights.component.yml) [![avm.res.insights.data-collection-endpoint](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.insights.data-collection-endpoint.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.insights.data-collection-endpoint.yml) [![avm.res.insights.diagnostic-setting](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.insights.diagnostic-setting.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.insights.diagnostic-setting.yml) [![avm.res.key-vault.vault](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.key-vault.vault.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.key-vault.vault.yml) [![avm.res.kubernetes-configuration.extension](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.kubernetes-configuration.extension.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.kubernetes-configuration.extension.yml) [![avm.res.kubernetes-configuration.flux-configuration](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.kubernetes-configuration.flux-configuration.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.kubernetes-configuration.flux-configuration.yml) [![avm.res.logic.workflow](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.logic.workflow.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.logic.workflow.yml) [![avm.res.managed-identity.user-assigned-identity](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.managed-identity.user-assigned-identity.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.managed-identity.user-assigned-identity.yml) [![avm.res.network.bastion-host](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.bastion-host.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.bastion-host.yml) [![avm.res.network.dns-forwarding-ruleset](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.dns-forwarding-ruleset.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.dns-forwarding-ruleset.yml) [![avm.res.network.dns-resolver](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.dns-resolver.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.dns-resolver.yml) [![avm.res.network.dns-zone](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.dns-zone.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.dns-zone.yml) [![avm.res.network.express-route-circuit](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.express-route-circuit.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.express-route-circuit.yml) [![avm.res.network.express-route-gateway](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.express-route-gateway.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.express-route-gateway.yml) [![avm.res.network.load-balancer](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.load-balancer.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.load-balancer.yml) [![avm.res.network.network-interface](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.network-interface.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.network-interface.yml) [![avm.res.network.private-dns-zone](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.private-dns-zone.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.private-dns-zone.yml) [![avm.res.network.private-endpoint](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.private-endpoint.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.private-endpoint.yml) [![avm.res.network.public-ip-address](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.public-ip-address.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.public-ip-address.yml) [![avm.res.network.public-ip-prefix](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.public-ip-prefix.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.public-ip-prefix.yml) [![avm.res.network.route-table](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.route-table.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.route-table.yml) [![avm.res.network.trafficmanagerprofile](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.trafficmanagerprofile.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.trafficmanagerprofile.yml) [![avm.res.network.virtual-network](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.virtual-network.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.network.virtual-network.yml) [![avm.res.operational-insights.workspace](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.operational-insights.workspace.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.operational-insights.workspace.yml) (unrelated) [![avm.res.operations-management.solution](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.operations-management.solution.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.operations-management.solution.yml) [![avm.res.power-bi-dedicated.capacity](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.power-bi-dedicated.capacity.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.power-bi-dedicated.capacity.yml) [![avm.res.resource-graph.query](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.resource-graph.query.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.resource-graph.query.yml) [![avm.res.resources.deployment-script](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.resources.deployment-script.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.resources.deployment-script.yml) [![avm.res.search.search-service](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.search.search-service.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.search.search-service.yml) [![avm.res.service-bus.namespace](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.service-bus.namespace.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.service-bus.namespace.yml) [![avm.res.sql.server](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.sql.server.yml/badge.svg?branch=users%2Falsehr%2FpipelineTest&event=workflow_dispatch)](https://github.com/AlexanderSehr/bicep-registry-modules/actions/workflows/avm.res.sql.server.yml) --------- Co-authored-by: Erika Gressi <56914614+eriqua@users.noreply.github.com> --- .../compliance/helper/helper.psm1 | 48 +++++++++++++++++++ .../compliance/module.tests.ps1 | 46 +++++++++++++----- .../tools/Invoke-WorkflowsForBranch.ps1 | 11 +++-- 3 files changed, 91 insertions(+), 14 deletions(-) diff --git a/avm/utilities/pipelines/staticValidation/compliance/helper/helper.psm1 b/avm/utilities/pipelines/staticValidation/compliance/helper/helper.psm1 index f176c4a707..5788e5b557 100644 --- a/avm/utilities/pipelines/staticValidation/compliance/helper/helper.psm1 +++ b/avm/utilities/pipelines/staticValidation/compliance/helper/helper.psm1 @@ -176,4 +176,52 @@ function Remove-JSONMetadata { } return $TemplateObject +} + +<# +.SYNOPSIS +Get a hashtable of all environment variables in the given GitHub workflow + +.DESCRIPTION +Get a hashtable of all environment variables in the given GitHub workflow + +.PARAMETER WorkflowPath +Mandatory. The path of the workflow to get the environment variables from + +.EXAMPLE +Get-WorkflowEnvVariablesAsObject -WorkflowPath 'C:/bicep-registry-modules/.github/workflows/test.yml' + +Get the environment variables from the given workflow +#> +function Get-WorkflowEnvVariablesAsObject { + + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string] $WorkflowPath + ) + + $contentFileContent = Get-Content -Path $workflowPath + + $envStartIndex = $contentFileContent.IndexOf('env:') + + if (-not $envStartIndex) { + # No env variables defined in the given workflow + return @{} + } + + $searchIndex = $envStartIndex + 1 + $envVars = @{} + + while ($searchIndex -lt $contentFileContent.Count) { + $line = $contentFileContent[$searchIndex] + if ($line -match "^\s+(\w+): (?:`"|')*([^`"'\s]+)(?:`"|')*$") { + $envVars[($Matches[1])] = $Matches[2] + } else { + break + } + $searchIndex++ + } + + return $envVars } \ No newline at end of file diff --git a/avm/utilities/pipelines/staticValidation/compliance/module.tests.ps1 b/avm/utilities/pipelines/staticValidation/compliance/module.tests.ps1 index 5a007c118a..9686ea9fc1 100644 --- a/avm/utilities/pipelines/staticValidation/compliance/module.tests.ps1 +++ b/avm/utilities/pipelines/staticValidation/compliance/module.tests.ps1 @@ -219,29 +219,53 @@ Describe 'File/folder tests' -Tag 'Modules' { Describe 'Pipeline tests' -Tag 'Pipeline' { - $moduleFolderTestCases = [System.Collections.ArrayList] @() + $pipelineTestCases = [System.Collections.ArrayList] @() foreach ($moduleFolderPath in $moduleFolderPaths) { $resourceTypeIdentifier = ($moduleFolderPath -split '[\/|\\]{1}avm[\/|\\]{1}(res|ptn)[\/|\\]{1}')[2] -replace '\\', '/' # avm/res// $relativeModulePath = Join-Path 'avm' ($moduleFolderPath -split '[\/|\\]{1}avm[\/|\\]{1}')[1] - $moduleFolderTestCases += @{ - moduleFolderName = $resourceTypeIdentifier - relativeModulePath = $relativeModulePath - isTopLevelModule = ($resourceTypeIdentifier -split '[\/|\\]').Count -eq 2 + $isTopLevelModule = ($resourceTypeIdentifier -split '[\/|\\]').Count -eq 2 + if ($isTopLevelModule) { + + $workflowsFolderName = Join-Path $repoRootPath '.github' 'workflows' + $workflowFileName = Get-PipelineFileName -ResourceIdentifier $relativeModulePath + $workflowPath = Join-Path $workflowsFolderName $workflowFileName + + $pipelineTestCases += @{ + relativeModulePath = $relativeModulePath + moduleFolderName = $resourceTypeIdentifier + workflowFileName = $workflowFileName + workflowPath = $workflowPath + } } } - It '[] Module should have a GitHub workflow.' -TestCases ($moduleFolderTestCases | Where-Object { $_.isTopLevelModule }) { + It '[] Module should have a GitHub workflow in path [.github/workflows/].' -TestCases $pipelineTestCases { param( - [string] $relativeModulePath + [string] $WorkflowPath ) - $workflowsFolderName = Join-Path $repoRootPath '.github' 'workflows' - $workflowFileName = Get-PipelineFileName -ResourceIdentifier $relativeModulePath - $workflowPath = Join-Path $workflowsFolderName $workflowFileName - Test-Path $workflowPath | Should -Be $true -Because "path [$workflowPath] should exist." + Test-Path $WorkflowPath | Should -Be $true -Because "path [$WorkflowPath] should exist." + } + + It '[] GitHub workflow [] should have [workflowPath] environment variable with value [.github/workflows/].' -TestCases $pipelineTestCases { + + param( + [string] $WorkflowPath, + [string] $WorkflowFileName + ) + + if (-not (Test-Path $WorkflowPath)) { + Set-ItResult -Skipped -Because "Cannot test content of file in path [$WorkflowPath] as it does not exist." + return + } + + $environmentVariables = Get-WorkflowEnvVariablesAsObject -WorkflowPath $WorkflowPath + + $environmentVariables.Keys | Should -Contain 'workflowPath' + $environmentVariables['workflowPath'] | Should -Be ".github/workflows/$workflowFileName" } } diff --git a/avm/utilities/tools/Invoke-WorkflowsForBranch.ps1 b/avm/utilities/tools/Invoke-WorkflowsForBranch.ps1 index 40736d1696..8777654978 100644 --- a/avm/utilities/tools/Invoke-WorkflowsForBranch.ps1 +++ b/avm/utilities/tools/Invoke-WorkflowsForBranch.ps1 @@ -172,6 +172,11 @@ Optional. The GitHub repository to run the workfows in. Invoke-WorkflowsForBranch -PersonalAccessToken '' -TargetBranch 'feature/branch' -PipelineFilter 'avm.res.*' -WorkflowInputs @{ staticValidation = 'true'; deploymentValidation = 'true'; removeDeployment = 'true' } Run all GitHub workflows that start with'avm.res.*' using branch 'feature/branch'. Also returns all GitHub status badges. + +.EXAMPLE +Invoke-WorkflowsForBranch -PersonalAccessToken '' -TargetBranch 'feature/branch' -PipelineFilter 'avm.res.*' -WorkflowInputs @{ staticValidation = 'true'; deploymentValidation = 'true'; removeDeployment = 'true' } -WhatIf + +Only simulate the triggering of all GitHub workflows that start with'avm.res.*' using branch 'feature/branch'. Hence ONLY returns all GitHub status badges. #> function Invoke-WorkflowsForBranch { @@ -224,10 +229,10 @@ function Invoke-WorkflowsForBranch { } # Generate pipeline badges - if ($SkipPipelineBadges) { + if (-not $SkipPipelineBadges) { $encodedBranch = [uri]::EscapeDataString($TargetBranch) - $workflowUrl = "https://github.com/$RepositoryOwner/$RepositoryName/actions/workflows/$workflowFileName&event=workflow_dispatch" - $gitHubWorkflowBadges += "[![$($workflow.name)]($workflowUrl/badge.svg?branch=$encodedBranch)]($workflowUrl)" + $workflowUrl = "https://github.com/$RepositoryOwner/$RepositoryName/actions/workflows/$workflowFileName" + $gitHubWorkflowBadges += "[![$($workflow.name)]($workflowUrl/badge.svg?branch=$encodedBranch&event=workflow_dispatch)]($workflowUrl)" } }