From 8dfba0efa053e8aab1b64cc04c4d74bf4a9da038 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:47:36 +0000 Subject: [PATCH 01/12] Initial plan From 5d230ac3b6f56c3cdb5a42d0349ff916e12e5c0d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:51:26 +0000 Subject: [PATCH 02/12] Add helper functions and refactor FederatedCredentials test to reuse repository Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- e2eTests/e2eTestHelper.psm1 | 85 ++++++++++++++++++ .../FederatedCredentials/runtest.ps1 | 86 +++++++++++++++---- 2 files changed, 156 insertions(+), 15 deletions(-) diff --git a/e2eTests/e2eTestHelper.psm1 b/e2eTests/e2eTestHelper.psm1 index d14b219bc1..d22d6476b5 100644 --- a/e2eTests/e2eTestHelper.psm1 +++ b/e2eTests/e2eTestHelper.psm1 @@ -326,6 +326,91 @@ function SetRepositorySecret { gh secret set $name -b $value --repo $repository } +function CleanupOldWorkflowRuns { + Param( + [string] $repository, + [int] $keepCount = 10 + ) + + if (!$repository) { + $repository = $defaultRepository + } + + Write-Host -ForegroundColor Yellow "`nCleaning up old workflow runs in $repository (keeping last $keepCount)" + + RefreshToken -repository $repository + + # Get all workflow runs + $runs = invoke-gh api /repos/$repository/actions/runs -silent -returnValue | ConvertFrom-Json + + if ($runs.workflow_runs.Count -le $keepCount) { + Write-Host "Found $($runs.workflow_runs.Count) workflow runs, no cleanup needed" + return + } + + # Get runs to delete (skip the most recent ones) + $runsToDelete = $runs.workflow_runs | Select-Object -Skip $keepCount + + Write-Host "Deleting $($runsToDelete.Count) old workflow runs..." + foreach ($run in $runsToDelete) { + try { + Write-Host "Deleting run $($run.id) ($($run.name) - $($run.status))" + invoke-gh api /repos/$repository/actions/runs/$($run.id) --method DELETE -silent | Out-Null + } + catch { + Write-Host "Warning: Failed to delete run $($run.id): $_" + } + } + Write-Host "Cleanup completed" +} + +function ResetRepositoryToSource { + Param( + [string] $repository, + [string] $sourceRepository, + [string] $branch = "main" + ) + + if (!$repository) { + $repository = $defaultRepository + } + + Write-Host -ForegroundColor Yellow "`nResetting repository $repository to match $sourceRepository" + + RefreshToken -repository $repository + + # Clone the repository locally if not already in it + $tempPath = [System.IO.Path]::GetTempPath() + $repoPath = Join-Path $tempPath ([System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetTempFileName())) + New-Item $repoPath -ItemType Directory | Out-Null + + Push-Location $repoPath + try { + Write-Host "Cloning $repository..." + invoke-gh repo clone $repository . -- --quiet + + # Fetch the source repository content + Write-Host "Fetching source repository $sourceRepository..." + invoke-git remote add source "https://github.com/$sourceRepository.git" + invoke-git fetch source $branch --quiet + + # Reset the current branch to match the source + Write-Host "Resetting $branch to match source/$branch..." + invoke-git checkout $branch --quiet + invoke-git reset --hard "source/$branch" --quiet + + # Force push to update the repository + Write-Host "Force pushing changes..." + invoke-git push origin $branch --force --quiet + + Write-Host "Repository reset completed successfully" + } + finally { + Pop-Location + Remove-Item -Path $repoPath -Force -Recurse -ErrorAction SilentlyContinue + } +} + function CreateNewAppInFolder { Param( [string] $folder, diff --git a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 index 3aad793fe8..fd6d98fde1 100644 --- a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 +++ b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 @@ -29,7 +29,9 @@ Write-Host -ForegroundColor Yellow @' # This test uses the bcsamples-bingmaps.appsource repository and will deliver a new build of the app to AppSource. # The bcsamples-bingmaps.appsource repository is setup to use an Azure KeyVault for secrets and app signing. # -# During the test, the bcsamples-bingmaps.appsource repository will be copied to a new repository called tmp-bingmaps.appsource. +# The test uses a stable temporary repository called tmp-bingmaps.appsource that is reused across test runs. +# This is required because federated credentials no longer work with repository name-based matching, +# so the repository must remain stable to maintain the federated credential configuration. # tmp-bingmaps.appsource has access to the same Azure KeyVault as bcsamples-bingmaps.appsource using federated credentials. # The bcSamples-bingmaps.appsource repository is setup for continuous delivery to AppSource # tmp-bingmaps.appsource also has access to the Entra ID app registration for delivering to AppSource using federated credentials. @@ -37,7 +39,8 @@ Write-Host -ForegroundColor Yellow @' # # This test tests the following scenario: # -# - Create a new repository called tmp-bingmaps.appsource (based on bcsamples-bingmaps.appsource) +# - Reuse or create the repository tmp-bingmaps.appsource (reset to match bcsamples-bingmaps.appsource if it exists) +# - Clean up old workflow runs to ensure proper workflow tracking # - Update AL-Go System Files in branch main in tmp-bingmaps.appsource # - Update version numbers in app.json in tmp-bingmaps.appsource in order to not be lower than the version number in AppSource (and not be higher than the next version from bcsamples-bingmaps.appsource) # - Wait for CI/CD in branch main in repository tmp-bingmaps.appsource @@ -59,31 +62,84 @@ $repository = "$githubOwner/tmp-bingmaps.appsource" $template = "https://github.com/$appSourceTemplate" $sourceRepository = 'microsoft/bcsamples-bingmaps.appsource' # E2E test will create a copy of this repository -# Create temp repository from sourceRepository +# Setup authentication and repository SetTokenAndRepository -github:$github -githubOwner $githubOwner -appId $e2eAppId -appKey $e2eAppKey -repository $repository +# Check if the repository already exists gh api repos/$repository --method HEAD -if ($LASTEXITCODE -eq 0) { - Write-Host "Repository $repository already exists. Deleting it." - gh repo delete $repository --yes | Out-Host - Start-Sleep -Seconds 30 +$repoExists = ($LASTEXITCODE -eq 0) + +if ($repoExists) { + # Repository exists - reuse it instead of deleting and recreating + # This is required because federated credentials no longer work with repository name-based matching, + # so the repository must remain stable across test runs + Write-Host "Repository $repository already exists. Reusing and resetting to match source." + + # Reset the repository to match the source repository + ResetRepositoryToSource -repository $repository -sourceRepository $sourceRepository -branch 'main' + + # Clean up old workflow runs to prevent the list from growing and ensure we wait for the correct run + CleanupOldWorkflowRuns -repository $repository -keepCount 5 + + # Update repository settings (in case they changed) + $tempPath = [System.IO.Path]::GetTempPath() + $repoPath = Join-Path $tempPath ([System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetTempFileName())) + New-Item $repoPath -ItemType Directory | Out-Null + Push-Location $repoPath + try { + invoke-gh repo clone $repository . -- --quiet + $repoSettingsFile = ".github\AL-Go-Settings.json" + if (Test-Path $repoSettingsFile) { + Add-PropertiesToJsonFile -path $repoSettingsFile -properties @{"ghTokenWorkflowSecretName" = "e2eghTokenWorkflow"} + invoke-git add $repoSettingsFile + invoke-git commit -m "Update repository settings for test" --quiet + invoke-git push --quiet + } + } + finally { + Pop-Location + Remove-Item -Path $repoPath -Force -Recurse -ErrorAction SilentlyContinue + } +} +else { + # Repository doesn't exist - create it + Write-Host "Repository $repository does not exist. Creating it." + CreateAlGoRepository ` + -github:$github ` + -template "https://github.com/$sourceRepository" ` + -repository $repository ` + -addRepoSettings @{"ghTokenWorkflowSecretName" = "e2eghTokenWorkflow" } } -CreateAlGoRepository ` - -github:$github ` - -template "https://github.com/$sourceRepository" ` - -repository $repository ` - -addRepoSettings @{"ghTokenWorkflowSecretName" = "e2eghTokenWorkflow" } - +# Always set/update secrets (they may have changed or repo may have been reset) SetRepositorySecret -repository $repository -name 'Azure_Credentials' -value $azureCredentials # Upgrade AL-Go System Files to test version -RunUpdateAlGoSystemFiles -directCommit -wait -templateUrl $template -repository $repository | Out-Null +# Capture the run object to ensure we wait for the correct workflow run +$updateRun = RunUpdateAlGoSystemFiles -directCommit -wait -templateUrl $template -repository $repository # Wait for CI/CD to complete +# The Update AL-Go System Files workflow triggers a CI/CD workflow via push event +# We need to wait for the CI/CD workflow that was triggered AFTER the update workflow completed +Write-Host "Waiting for CI/CD workflow to start (triggered by Update AL-Go System Files)..." Start-Sleep -Seconds 60 + +# Get workflow runs that started after the update workflow +$updateCompletedAt = [DateTime]$updateRun.updated_at $runs = invoke-gh api /repos/$repository/actions/runs -silent -returnValue | ConvertFrom-Json -$run = $runs.workflow_runs | Select-Object -First 1 + +# Find the CI/CD workflow run that started after the update workflow completed +$run = $runs.workflow_runs | Where-Object { + $_.event -eq 'push' -and [DateTime]$_.created_at -gt $updateCompletedAt +} | Select-Object -First 1 + +if (-not $run) { + # Fallback to the first workflow run if we can't find one based on timestamp + Write-Host "Warning: Could not find CI/CD run based on timestamp, using first run" + $run = $runs.workflow_runs | Select-Object -First 1 +} + +Write-Host "Waiting for CI/CD workflow run $($run.id) to complete..." WaitWorkflow -repository $repository -runid $run.id -noError # The CI/CD workflow should fail because the version number of the app in thie repository is lower than the version number in AppSource From 0cccc745594b7dab19729a36064df2fc40312242 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:52:58 +0000 Subject: [PATCH 03/12] Reorganize repository settings update logic in runtest.ps1 Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- .../FederatedCredentials/runtest.ps1 | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 index fd6d98fde1..aa414a069a 100644 --- a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 +++ b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 @@ -80,13 +80,28 @@ if ($repoExists) { # Clean up old workflow runs to prevent the list from growing and ensure we wait for the correct run CleanupOldWorkflowRuns -repository $repository -keepCount 5 - - # Update repository settings (in case they changed) +} +else { + # Repository doesn't exist - create it + Write-Host "Repository $repository does not exist. Creating it." + CreateAlGoRepository ` + -github:$github ` + -template "https://github.com/$sourceRepository" ` + -repository $repository ` + -addRepoSettings @{"ghTokenWorkflowSecretName" = "e2eghTokenWorkflow" } +} + +# Always set/update secrets (they may have changed or repo may have been reset) +SetRepositorySecret -repository $repository -name 'Azure_Credentials' -value $azureCredentials + +# When repo is reused, we need to re-apply the custom repository settings that were lost during reset +if ($repoExists) { $tempPath = [System.IO.Path]::GetTempPath() $repoPath = Join-Path $tempPath ([System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetTempFileName())) New-Item $repoPath -ItemType Directory | Out-Null Push-Location $repoPath try { + Write-Host "Re-applying repository settings..." invoke-gh repo clone $repository . -- --quiet $repoSettingsFile = ".github\AL-Go-Settings.json" if (Test-Path $repoSettingsFile) { @@ -101,18 +116,6 @@ if ($repoExists) { Remove-Item -Path $repoPath -Force -Recurse -ErrorAction SilentlyContinue } } -else { - # Repository doesn't exist - create it - Write-Host "Repository $repository does not exist. Creating it." - CreateAlGoRepository ` - -github:$github ` - -template "https://github.com/$sourceRepository" ` - -repository $repository ` - -addRepoSettings @{"ghTokenWorkflowSecretName" = "e2eghTokenWorkflow" } -} - -# Always set/update secrets (they may have changed or repo may have been reset) -SetRepositorySecret -repository $repository -name 'Azure_Credentials' -value $azureCredentials # Upgrade AL-Go System Files to test version # Capture the run object to ensure we wait for the correct workflow run From 817d4a8918d5b53deadb1441f1c827a498f53ece Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:57:05 +0000 Subject: [PATCH 04/12] Address code review feedback: fix temp paths, add error handling, improve pagination and timestamp comparison Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- e2eTests/e2eTestHelper.psm1 | 31 +++++++++++++++++-- .../FederatedCredentials/runtest.ps1 | 14 ++++++--- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/e2eTests/e2eTestHelper.psm1 b/e2eTests/e2eTestHelper.psm1 index d22d6476b5..a0bf6ef95b 100644 --- a/e2eTests/e2eTestHelper.psm1 +++ b/e2eTests/e2eTestHelper.psm1 @@ -340,8 +340,8 @@ function CleanupOldWorkflowRuns { RefreshToken -repository $repository - # Get all workflow runs - $runs = invoke-gh api /repos/$repository/actions/runs -silent -returnValue | ConvertFrom-Json + # Get all workflow runs, sorted by created_at descending (newest first) + $runs = invoke-gh api "/repos/$repository/actions/runs?per_page=100" -silent -returnValue | ConvertFrom-Json if ($runs.workflow_runs.Count -le $keepCount) { Write-Host "Found $($runs.workflow_runs.Count) workflow runs, no cleanup needed" @@ -349,6 +349,7 @@ function CleanupOldWorkflowRuns { } # Get runs to delete (skip the most recent ones) + # Runs are already sorted by created_at descending from the API $runsToDelete = $runs.workflow_runs | Select-Object -Skip $keepCount Write-Host "Deleting $($runsToDelete.Count) old workflow runs..." @@ -381,30 +382,54 @@ function ResetRepositoryToSource { # Clone the repository locally if not already in it $tempPath = [System.IO.Path]::GetTempPath() - $repoPath = Join-Path $tempPath ([System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetTempFileName())) + $repoPath = Join-Path $tempPath ([System.Guid]::NewGuid().ToString()) New-Item $repoPath -ItemType Directory | Out-Null Push-Location $repoPath try { Write-Host "Cloning $repository..." invoke-gh repo clone $repository . -- --quiet + if ($LASTEXITCODE -ne 0) { + throw "Failed to clone repository $repository" + } # Fetch the source repository content Write-Host "Fetching source repository $sourceRepository..." invoke-git remote add source "https://github.com/$sourceRepository.git" + if ($LASTEXITCODE -ne 0) { + throw "Failed to add remote source for $sourceRepository" + } + invoke-git fetch source $branch --quiet + if ($LASTEXITCODE -ne 0) { + throw "Failed to fetch branch $branch from source $sourceRepository" + } # Reset the current branch to match the source Write-Host "Resetting $branch to match source/$branch..." invoke-git checkout $branch --quiet + if ($LASTEXITCODE -ne 0) { + throw "Failed to checkout branch $branch" + } + invoke-git reset --hard "source/$branch" --quiet + if ($LASTEXITCODE -ne 0) { + throw "Failed to reset branch $branch to source/$branch" + } # Force push to update the repository Write-Host "Force pushing changes..." invoke-git push origin $branch --force --quiet + if ($LASTEXITCODE -ne 0) { + throw "Failed to push changes to $repository" + } Write-Host "Repository reset completed successfully" } + catch { + Write-Host "Error resetting repository: $_" + throw + } finally { Pop-Location Remove-Item -Path $repoPath -Force -Recurse -ErrorAction SilentlyContinue diff --git a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 index aa414a069a..5bbfa581cc 100644 --- a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 +++ b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 @@ -97,7 +97,7 @@ SetRepositorySecret -repository $repository -name 'Azure_Credentials' -value $az # When repo is reused, we need to re-apply the custom repository settings that were lost during reset if ($repoExists) { $tempPath = [System.IO.Path]::GetTempPath() - $repoPath = Join-Path $tempPath ([System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetTempFileName())) + $repoPath = Join-Path $tempPath ([System.Guid]::NewGuid().ToString()) New-Item $repoPath -ItemType Directory | Out-Null Push-Location $repoPath try { @@ -110,6 +110,9 @@ if ($repoExists) { invoke-git commit -m "Update repository settings for test" --quiet invoke-git push --quiet } + else { + Write-Host "Warning: .github\AL-Go-Settings.json not found after cloning. Settings may not be applied correctly." + } } finally { Pop-Location @@ -128,12 +131,13 @@ Write-Host "Waiting for CI/CD workflow to start (triggered by Update AL-Go Syste Start-Sleep -Seconds 60 # Get workflow runs that started after the update workflow -$updateCompletedAt = [DateTime]$updateRun.updated_at +# Use created_at for consistent timestamp comparison, and add a small buffer for timing precision +$updateCreatedAt = [DateTime]$updateRun.created_at $runs = invoke-gh api /repos/$repository/actions/runs -silent -returnValue | ConvertFrom-Json -# Find the CI/CD workflow run that started after the update workflow completed +# Find the CI/CD workflow run that started after the update workflow was created $run = $runs.workflow_runs | Where-Object { - $_.event -eq 'push' -and [DateTime]$_.created_at -gt $updateCompletedAt + $_.event -eq 'push' -and [DateTime]$_.created_at -gt $updateCreatedAt } | Select-Object -First 1 if (-not $run) { @@ -145,7 +149,7 @@ if (-not $run) { Write-Host "Waiting for CI/CD workflow run $($run.id) to complete..." WaitWorkflow -repository $repository -runid $run.id -noError -# The CI/CD workflow should fail because the version number of the app in thie repository is lower than the version number in AppSource +# The CI/CD workflow should fail because the version number of the app in the repository is lower than the version number in AppSource # Reason being that major.minor from the original bcsamples-bingmaps.appsource is the same and the build number in the newly created repository is lower than the one in AppSource # This error is expected we will grab the version number from AppSource, add one to revision number (by switching to versioningstrategy 3 in the tmp repo) and use it in the next run $MatchArr = Test-LogContainsFromRun -repository $repository -runid $run.id -jobName 'Deliver to AppSource' -stepName 'Deliver' -expectedText '(?m)^.*The new version number \((\d+(?:\.\d+){3})\) is lower than the existing version number \((\d+(?:\.\d+){3})\) in Partner Center.*$' -isRegEx From 3caaa49684a1ea491e5ddc61e0c314899ea7f67d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:03:00 +0000 Subject: [PATCH 05/12] Remove repository creation fallback - require pre-existing repo with federated credentials Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- .../FederatedCredentials/runtest.ps1 | 84 +++++++++---------- 1 file changed, 38 insertions(+), 46 deletions(-) diff --git a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 index 5bbfa581cc..d6a846cf38 100644 --- a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 +++ b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 @@ -29,7 +29,8 @@ Write-Host -ForegroundColor Yellow @' # This test uses the bcsamples-bingmaps.appsource repository and will deliver a new build of the app to AppSource. # The bcsamples-bingmaps.appsource repository is setup to use an Azure KeyVault for secrets and app signing. # -# The test uses a stable temporary repository called tmp-bingmaps.appsource that is reused across test runs. +# The test requires a stable temporary repository called tmp-bingmaps.appsource that must be manually created +# with federated credentials configured before running this test. # This is required because federated credentials no longer work with repository name-based matching, # so the repository must remain stable to maintain the federated credential configuration. # tmp-bingmaps.appsource has access to the same Azure KeyVault as bcsamples-bingmaps.appsource using federated credentials. @@ -39,7 +40,8 @@ Write-Host -ForegroundColor Yellow @' # # This test tests the following scenario: # -# - Reuse or create the repository tmp-bingmaps.appsource (reset to match bcsamples-bingmaps.appsource if it exists) +# - Verify that the repository tmp-bingmaps.appsource exists (error out if not) +# - Reset the repository to match bcsamples-bingmaps.appsource for deterministic state # - Clean up old workflow runs to ensure proper workflow tracking # - Update AL-Go System Files in branch main in tmp-bingmaps.appsource # - Update version numbers in app.json in tmp-bingmaps.appsource in order to not be lower than the version number in AppSource (and not be higher than the next version from bcsamples-bingmaps.appsource) @@ -66,59 +68,49 @@ $sourceRepository = 'microsoft/bcsamples-bingmaps.appsource' # E2E test will cre SetTokenAndRepository -github:$github -githubOwner $githubOwner -appId $e2eAppId -appKey $e2eAppKey -repository $repository # Check if the repository already exists +# This repository must exist with federated credentials already configured gh api repos/$repository --method HEAD -$repoExists = ($LASTEXITCODE -eq 0) - -if ($repoExists) { - # Repository exists - reuse it instead of deleting and recreating - # This is required because federated credentials no longer work with repository name-based matching, - # so the repository must remain stable across test runs - Write-Host "Repository $repository already exists. Reusing and resetting to match source." - - # Reset the repository to match the source repository - ResetRepositoryToSource -repository $repository -sourceRepository $sourceRepository -branch 'main' - - # Clean up old workflow runs to prevent the list from growing and ensure we wait for the correct run - CleanupOldWorkflowRuns -repository $repository -keepCount 5 -} -else { - # Repository doesn't exist - create it - Write-Host "Repository $repository does not exist. Creating it." - CreateAlGoRepository ` - -github:$github ` - -template "https://github.com/$sourceRepository" ` - -repository $repository ` - -addRepoSettings @{"ghTokenWorkflowSecretName" = "e2eghTokenWorkflow" } +if ($LASTEXITCODE -ne 0) { + throw "Repository $repository does not exist. The repository must be created manually with federated credentials configured before running this test." } +# Repository exists - reuse it and reset to source state +# This is required because federated credentials no longer work with repository name-based matching, +# so the repository must remain stable across test runs +Write-Host "Repository $repository exists. Reusing and resetting to match source." + +# Reset the repository to match the source repository +ResetRepositoryToSource -repository $repository -sourceRepository $sourceRepository -branch 'main' + +# Clean up old workflow runs to prevent the list from growing and ensure we wait for the correct run +CleanupOldWorkflowRuns -repository $repository -keepCount 5 + # Always set/update secrets (they may have changed or repo may have been reset) SetRepositorySecret -repository $repository -name 'Azure_Credentials' -value $azureCredentials -# When repo is reused, we need to re-apply the custom repository settings that were lost during reset -if ($repoExists) { - $tempPath = [System.IO.Path]::GetTempPath() - $repoPath = Join-Path $tempPath ([System.Guid]::NewGuid().ToString()) - New-Item $repoPath -ItemType Directory | Out-Null - Push-Location $repoPath - try { - Write-Host "Re-applying repository settings..." - invoke-gh repo clone $repository . -- --quiet - $repoSettingsFile = ".github\AL-Go-Settings.json" - if (Test-Path $repoSettingsFile) { - Add-PropertiesToJsonFile -path $repoSettingsFile -properties @{"ghTokenWorkflowSecretName" = "e2eghTokenWorkflow"} - invoke-git add $repoSettingsFile - invoke-git commit -m "Update repository settings for test" --quiet - invoke-git push --quiet - } - else { - Write-Host "Warning: .github\AL-Go-Settings.json not found after cloning. Settings may not be applied correctly." - } +# Re-apply the custom repository settings that were lost during reset +$tempPath = [System.IO.Path]::GetTempPath() +$repoPath = Join-Path $tempPath ([System.Guid]::NewGuid().ToString()) +New-Item $repoPath -ItemType Directory | Out-Null +Push-Location $repoPath +try { + Write-Host "Re-applying repository settings..." + invoke-gh repo clone $repository . -- --quiet + $repoSettingsFile = ".github\AL-Go-Settings.json" + if (Test-Path $repoSettingsFile) { + Add-PropertiesToJsonFile -path $repoSettingsFile -properties @{"ghTokenWorkflowSecretName" = "e2eghTokenWorkflow"} + invoke-git add $repoSettingsFile + invoke-git commit -m "Update repository settings for test" --quiet + invoke-git push --quiet } - finally { - Pop-Location - Remove-Item -Path $repoPath -Force -Recurse -ErrorAction SilentlyContinue + else { + Write-Host "Warning: .github\AL-Go-Settings.json not found after cloning. Settings may not be applied correctly." } } +finally { + Pop-Location + Remove-Item -Path $repoPath -Force -Recurse -ErrorAction SilentlyContinue +} # Upgrade AL-Go System Files to test version # Capture the run object to ensure we wait for the correct workflow run From 23094022fe1db1cb9086c640c57b300f8cb9c00d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:23:34 +0000 Subject: [PATCH 06/12] Address PR feedback: make params mandatory, remove keepCount, rename function, exclude tmp-bingmaps from cleanup Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- Internal/Scripts/RemoveTempRepos.ps1 | 2 +- e2eTests/e2eTestHelper.psm1 | 32 +++++++------------ .../FederatedCredentials/runtest.ps1 | 4 +-- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/Internal/Scripts/RemoveTempRepos.ps1 b/Internal/Scripts/RemoveTempRepos.ps1 index 803264301d..ee417d911c 100644 --- a/Internal/Scripts/RemoveTempRepos.ps1 +++ b/Internal/Scripts/RemoveTempRepos.ps1 @@ -6,7 +6,7 @@ param( $ErrorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 Import-Module (Join-Path "." "e2eTests/e2eTestHelper.psm1") -DisableNameChecking -@(invoke-gh repo list $githubOwner --limit 1000 -silent -returnValue) | ForEach-Object { $_.Split("`t")[0] } | Where-Object { "$_" -like "$githubOwner/tmp*" } | ForEach-Object { +@(invoke-gh repo list $githubOwner --limit 1000 -silent -returnValue) | ForEach-Object { $_.Split("`t")[0] } | Where-Object { "$_" -like "$githubOwner/tmp*" -and "$_" -ne "$githubOwner/tmp-bingmaps.appsource" } | ForEach-Object { $repo = $_ Write-Host "https://github.com/$repo" $repoOwner = $repo.Split('/')[0] diff --git a/e2eTests/e2eTestHelper.psm1 b/e2eTests/e2eTestHelper.psm1 index a0bf6ef95b..00a82c8f94 100644 --- a/e2eTests/e2eTestHelper.psm1 +++ b/e2eTests/e2eTestHelper.psm1 @@ -326,34 +326,26 @@ function SetRepositorySecret { gh secret set $name -b $value --repo $repository } -function CleanupOldWorkflowRuns { +function CleanupWorkflowRuns { Param( - [string] $repository, - [int] $keepCount = 10 + [Parameter(Mandatory = $true)] + [string] $repository ) - if (!$repository) { - $repository = $defaultRepository - } - - Write-Host -ForegroundColor Yellow "`nCleaning up old workflow runs in $repository (keeping last $keepCount)" + Write-Host -ForegroundColor Yellow "`nCleaning up workflow runs in $repository" RefreshToken -repository $repository - # Get all workflow runs, sorted by created_at descending (newest first) + # Get all workflow runs $runs = invoke-gh api "/repos/$repository/actions/runs?per_page=100" -silent -returnValue | ConvertFrom-Json - if ($runs.workflow_runs.Count -le $keepCount) { - Write-Host "Found $($runs.workflow_runs.Count) workflow runs, no cleanup needed" + if ($runs.workflow_runs.Count -eq 0) { + Write-Host "No workflow runs found" return } - # Get runs to delete (skip the most recent ones) - # Runs are already sorted by created_at descending from the API - $runsToDelete = $runs.workflow_runs | Select-Object -Skip $keepCount - - Write-Host "Deleting $($runsToDelete.Count) old workflow runs..." - foreach ($run in $runsToDelete) { + Write-Host "Deleting $($runs.workflow_runs.Count) workflow runs..." + foreach ($run in $runs.workflow_runs) { try { Write-Host "Deleting run $($run.id) ($($run.name) - $($run.status))" invoke-gh api /repos/$repository/actions/runs/$($run.id) --method DELETE -silent | Out-Null @@ -367,15 +359,13 @@ function CleanupOldWorkflowRuns { function ResetRepositoryToSource { Param( + [Parameter(Mandatory = $true)] [string] $repository, + [Parameter(Mandatory = $true)] [string] $sourceRepository, [string] $branch = "main" ) - if (!$repository) { - $repository = $defaultRepository - } - Write-Host -ForegroundColor Yellow "`nResetting repository $repository to match $sourceRepository" RefreshToken -repository $repository diff --git a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 index d6a846cf38..939fd0aae5 100644 --- a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 +++ b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 @@ -82,8 +82,8 @@ Write-Host "Repository $repository exists. Reusing and resetting to match source # Reset the repository to match the source repository ResetRepositoryToSource -repository $repository -sourceRepository $sourceRepository -branch 'main' -# Clean up old workflow runs to prevent the list from growing and ensure we wait for the correct run -CleanupOldWorkflowRuns -repository $repository -keepCount 5 +# Clean up workflow runs to ensure proper workflow tracking +CleanupWorkflowRuns -repository $repository # Always set/update secrets (they may have changed or repo may have been reset) SetRepositorySecret -repository $repository -name 'Azure_Credentials' -value $azureCredentials From 094ce51ede7a883d2becc9d032841b6c739abe64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:28:48 +0000 Subject: [PATCH 07/12] Rename repository from tmp-bingmaps.appsource to e2e-bingmaps.appsource to avoid auto-cleanup Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- Internal/Scripts/RemoveTempRepos.ps1 | 2 +- .../scenarios/FederatedCredentials/runtest.ps1 | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Internal/Scripts/RemoveTempRepos.ps1 b/Internal/Scripts/RemoveTempRepos.ps1 index ee417d911c..803264301d 100644 --- a/Internal/Scripts/RemoveTempRepos.ps1 +++ b/Internal/Scripts/RemoveTempRepos.ps1 @@ -6,7 +6,7 @@ param( $ErrorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 Import-Module (Join-Path "." "e2eTests/e2eTestHelper.psm1") -DisableNameChecking -@(invoke-gh repo list $githubOwner --limit 1000 -silent -returnValue) | ForEach-Object { $_.Split("`t")[0] } | Where-Object { "$_" -like "$githubOwner/tmp*" -and "$_" -ne "$githubOwner/tmp-bingmaps.appsource" } | ForEach-Object { +@(invoke-gh repo list $githubOwner --limit 1000 -silent -returnValue) | ForEach-Object { $_.Split("`t")[0] } | Where-Object { "$_" -like "$githubOwner/tmp*" } | ForEach-Object { $repo = $_ Write-Host "https://github.com/$repo" $repoOwner = $repo.Split('/')[0] diff --git a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 index 939fd0aae5..77a4bdb707 100644 --- a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 +++ b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 @@ -29,23 +29,23 @@ Write-Host -ForegroundColor Yellow @' # This test uses the bcsamples-bingmaps.appsource repository and will deliver a new build of the app to AppSource. # The bcsamples-bingmaps.appsource repository is setup to use an Azure KeyVault for secrets and app signing. # -# The test requires a stable temporary repository called tmp-bingmaps.appsource that must be manually created +# The test requires a stable temporary repository called e2e-bingmaps.appsource that must be manually created # with federated credentials configured before running this test. # This is required because federated credentials no longer work with repository name-based matching, # so the repository must remain stable to maintain the federated credential configuration. -# tmp-bingmaps.appsource has access to the same Azure KeyVault as bcsamples-bingmaps.appsource using federated credentials. +# e2e-bingmaps.appsource has access to the same Azure KeyVault as bcsamples-bingmaps.appsource using federated credentials. # The bcSamples-bingmaps.appsource repository is setup for continuous delivery to AppSource -# tmp-bingmaps.appsource also has access to the Entra ID app registration for delivering to AppSource using federated credentials. +# e2e-bingmaps.appsource also has access to the Entra ID app registration for delivering to AppSource using federated credentials. # This test will deliver another build of the latest app version already delivered to AppSource (without go-live) # # This test tests the following scenario: # -# - Verify that the repository tmp-bingmaps.appsource exists (error out if not) +# - Verify that the repository e2e-bingmaps.appsource exists (error out if not) # - Reset the repository to match bcsamples-bingmaps.appsource for deterministic state # - Clean up old workflow runs to ensure proper workflow tracking -# - Update AL-Go System Files in branch main in tmp-bingmaps.appsource -# - Update version numbers in app.json in tmp-bingmaps.appsource in order to not be lower than the version number in AppSource (and not be higher than the next version from bcsamples-bingmaps.appsource) -# - Wait for CI/CD in branch main in repository tmp-bingmaps.appsource +# - Update AL-Go System Files in branch main in e2e-bingmaps.appsource +# - Update version numbers in app.json in e2e-bingmaps.appsource in order to not be lower than the version number in AppSource (and not be higher than the next version from bcsamples-bingmaps.appsource) +# - Wait for CI/CD in branch main in repository e2e-bingmaps.appsource # - Check that artifacts are created and signed # - Check that the app is delivered to AppSource '@ @@ -60,7 +60,7 @@ if ($linux) { Remove-Module e2eTestHelper -ErrorAction SilentlyContinue Import-Module (Join-Path $PSScriptRoot "..\..\e2eTestHelper.psm1") -DisableNameChecking -$repository = "$githubOwner/tmp-bingmaps.appsource" +$repository = "$githubOwner/e2e-bingmaps.appsource" $template = "https://github.com/$appSourceTemplate" $sourceRepository = 'microsoft/bcsamples-bingmaps.appsource' # E2E test will create a copy of this repository From 626a7918b2a075d32a07e091ede51ffb04582b63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:10:08 +0000 Subject: [PATCH 08/12] Add documentation to new helper functions and remove trailing whitespaces Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- e2eTests/e2eTestHelper.psm1 | 35 +++++++++++++++++++ .../FederatedCredentials/runtest.ps1 | 4 +-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/e2eTests/e2eTestHelper.psm1 b/e2eTests/e2eTestHelper.psm1 index 00a82c8f94..7e13c99a98 100644 --- a/e2eTests/e2eTestHelper.psm1 +++ b/e2eTests/e2eTestHelper.psm1 @@ -326,6 +326,20 @@ function SetRepositorySecret { gh secret set $name -b $value --repo $repository } +<# +.SYNOPSIS + Deletes all workflow runs in a repository. + +.DESCRIPTION + Cleans up workflow runs in a GitHub repository by deleting all existing runs. + This is useful for ensuring a clean state before running tests that track specific workflow runs. + +.PARAMETER repository + The full repository name in the format "owner/repo" (e.g., "microsoft/AL-Go"). + +.EXAMPLE + CleanupWorkflowRuns -repository "microsoft/AL-Go" +#> function CleanupWorkflowRuns { Param( [Parameter(Mandatory = $true)] @@ -357,6 +371,27 @@ function CleanupWorkflowRuns { Write-Host "Cleanup completed" } +<# +.SYNOPSIS + Resets a repository to match the content of a source repository. + +.DESCRIPTION + Clones the target repository, fetches content from a source repository, and performs a hard reset + followed by a force push. This preserves the repository identity while resetting its content to + match the source repository. Useful for ensuring deterministic state in end-to-end tests. + +.PARAMETER repository + The full name of the target repository to reset in the format "owner/repo" (e.g., "microsoft/AL-Go"). + +.PARAMETER sourceRepository + The full name of the source repository to copy content from in the format "owner/repo". + +.PARAMETER branch + The branch name to reset. Defaults to "main". + +.EXAMPLE + ResetRepositoryToSource -repository "microsoft/test-repo" -sourceRepository "microsoft/source-repo" -branch "main" +#> function ResetRepositoryToSource { Param( [Parameter(Mandatory = $true)] diff --git a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 index 77a4bdb707..f84399e02b 100644 --- a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 +++ b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 @@ -128,8 +128,8 @@ $updateCreatedAt = [DateTime]$updateRun.created_at $runs = invoke-gh api /repos/$repository/actions/runs -silent -returnValue | ConvertFrom-Json # Find the CI/CD workflow run that started after the update workflow was created -$run = $runs.workflow_runs | Where-Object { - $_.event -eq 'push' -and [DateTime]$_.created_at -gt $updateCreatedAt +$run = $runs.workflow_runs | Where-Object { + $_.event -eq 'push' -and [DateTime]$_.created_at -gt $updateCreatedAt } | Select-Object -First 1 if (-not $run) { From 6b92fdee124f9a5bfd43980ed3c290526c366f22 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:01:29 +0000 Subject: [PATCH 09/12] Remove all trailing whitespaces from e2eTestHelper.psm1 Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- e2eTests/e2eTestHelper.psm1 | 1788 +++++++++++++++++------------------ 1 file changed, 894 insertions(+), 894 deletions(-) diff --git a/e2eTests/e2eTestHelper.psm1 b/e2eTests/e2eTestHelper.psm1 index 7e13c99a98..c62c724afe 100644 --- a/e2eTests/e2eTestHelper.psm1 +++ b/e2eTests/e2eTestHelper.psm1 @@ -1,894 +1,894 @@ -$githubOwner = "githubOwner" -$token = "DefaultToken" -$defaultRepository = "repo" -$defaultApplication = "22.0.0.0" -$defaultRuntime = "10.0" -$defaultPublisher = "MS Test" -$lastTokenRefresh = 0 - -Import-Module (Join-Path $PSScriptRoot "..\Actions\Github-Helper.psm1" -Resolve) -DisableNameChecking -Global -. (Join-Path $PSScriptRoot "..\Actions\AL-Go-Helper.ps1" -Resolve) - -function GetDefaultPublisher() { - return $defaultPublisher -} - -function SetTokenAndRepository { - Param( - [string] $githubOwner, - [string] $token, - [string] $appId, - [string] $appKey, - [string] $repository, - [switch] $github - ) - - $script:githubOwner = $githubOwner - $script:defaultRepository = $repository - - if ($github) { - invoke-git config --global user.email "$githubOwner@users.noreply.github.com" - invoke-git config --global user.name "$githubOwner" - invoke-git config --global hub.protocol https - invoke-git config --global core.autocrlf false - } - - if (-not $github) { - # Running locally - Ensure the user is authenticated with the GitHub CLI. - # This is required for local runs to perform GitHub-related operations. - invoke-gh auth status - gh auth refresh --scopes repo,admin:org,workflow,write:packages,read:packages,delete:packages,user,delete_repo - } elseif ($appKey -and $appId) { - # Running in GitHub Actions - $token = @{ "GitHubAppClientId" = $appId; "PrivateKey" = ($appKey -join '') } | ConvertTo-Json -Compress -Depth 99 - } else { - throw "GitHub App ID and Private Key not set. In order to run end to end tests, you need a Secret called E2E_PRIVATE_KEY and a variable called E2E_APP_ID." - } - - # Repository isn't created yet so authenticating towards the .github repository - RefreshToken -token $token -repository "$githubOwner/.github" -} - -function RefreshToken { - Param( - [Parameter(Mandatory = $false)] - [string] $token, - [Parameter(Mandatory = $true)] - [string] $repository, - [Parameter(Mandatory = $false)] - [switch] $force - ) - if ($github) { - if ($token) { - $script:token = $token - } - - if ($script:token -eq "DefaultToken") { - throw "Token not set." - } - - # Check if the last token refresh was more than 10 minutes ago - - if ((-not $force) -and ($script:lastTokenRefresh -ne 0) -and (([DateTime]::Now - $script:lastTokenRefresh).TotalMinutes -lt 10)) { - return - } - - Write-Host "Authenticating with GitHub using token" - $realToken = GetAccessToken -token $script:token -repository $repository -repositories @() - $script:lastTokenRefresh = [DateTime]::Now - $ENV:GITHUB_TOKEN = $realToken - $ENV:GH_TOKEN = $realToken - invoke-gh auth setup-git # Use GitHub CLI as a credential helper - } else { - $realToken = gh auth token - $ENV:GITHUB_TOKEN = $realToken - $ENV:GH_TOKEN = $realToken - invoke-gh auth setup-git # Use GitHub CLI as a credential helper - } -} - -function Add-PropertiesToJsonFile { - Param( - [string] $path, - [hashTable] $properties, - [switch] $commit, - [switch] $wait - ) - - Write-Host -ForegroundColor Yellow "`nAdd Properties to $([System.IO.Path]::GetFileName($path))" - Write-Host "Properties" - $properties | Out-Host - - $json = Get-Content $path -Encoding UTF8 | ConvertFrom-Json | ConvertTo-HashTable -recurse - $properties.Keys | ForEach-Object { - $json."$_" = $properties."$_" - } - $json | Set-JsonContentLF -path $path - - if ($commit) { - CommitAndPush -commitMessage "Add properties to $([System.IO.Path]::GetFileName($path))" -wait:$wait - } -} - -function Remove-PropertiesFromJsonFile { - Param( - [string] $path, - [string[]] $properties, - [switch] $commit, - [switch] $wait - ) - - Write-Host -ForegroundColor Yellow "`nRemove Properties from $([System.IO.Path]::GetFileName($path))" - Write-Host "Properties" - $properties | Out-Host - - $json = Get-Content $path -Encoding UTF8 | ConvertFrom-Json | ConvertTo-HashTable -recurse - $keys = @($json.Keys) - $keys | ForEach-Object { - $key = $_ - if ($properties | Where-Object { $key -like $_ }) { - $json.Remove($key) - } - } - $json | Set-JsonContentLF -path $path - - if ($commit) { - CommitAndPush -commitMessage "Remove properties from $([System.IO.Path]::GetFileName($path))" -wait:$wait - } -} - -function RunWorkflow { - Param( - [string] $name, - [hashtable] $parameters = @{}, - [switch] $wait, - [string] $repository, - [string] $branch = "main" - ) - - if (!$repository) { - $repository = $defaultRepository - } - Write-Host -ForegroundColor Yellow "`nRun workflow $($name.Trim()) in $repository" - if ($parameters -and $parameters.Count -gt 0) { - Write-Host "Parameters:" - Write-Host ($parameters | ConvertTo-Json) - } - - RefreshToken -repository $repository - - $headers = GetHeaders -token $Env:GH_TOKEN -repository $repository - WaitForRateLimit -headers $headers -displayStatus - - Write-Host "Get Workflows" - $url = "https://api.github.com/repos/$repository/actions/workflows" - $workflows = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflows - $workflows | ForEach-Object { Write-Host "- $($_.Name)"} - if (!$workflows) { - Write-Host "No workflows found, waiting 60 seconds and retrying" - Start-Sleep -seconds 60 - $workflows = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflows - $workflows | ForEach-Object { Write-Host "- $($_.Name)"} - if (!$workflows) { - throw "No workflows found" - } - } - $workflow = $workflows | Where-Object { $_.Name.Trim() -eq $name } - if (!$workflow) { - throw "Workflow $name not found" - } - - Write-Host "Get Previous runs" - $url = "https://api.github.com/repos/$repository/actions/runs" - $previousrun = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.workflow_id -eq $workflow.id -and $_.event -eq 'workflow_dispatch' } | Select-Object -First 1 - if ($previousrun) { - Write-Host "Previous run: $($previousrun.id)" - } - else { - Write-Host "No previous run found" - } - - Write-Host "Run workflow" - $url = "https://api.github.com/repos/$repository/actions/workflows/$($workflow.id)/dispatches" - Write-Host $url - $body = @{ - "ref" = "refs/heads/$branch" - "inputs" = $parameters - } - InvokeWebRequest -Method Post -Headers $headers -Uri $url -Body ($body | ConvertTo-Json) | Out-Null - - Write-Host "Queuing" - do { - Start-Sleep -Seconds 10 - $url = "https://api.github.com/repos/$repository/actions/runs" - $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.workflow_id -eq $workflow.id -and $_.event -eq 'workflow_dispatch' } | Select-Object -First 1 - Write-Host "." - } until (($run) -and ((!$previousrun) -or ($run.id -ne $previousrun.id))) - $runid = $run.id - Write-Host "Run URL: https://github.com/$repository/actions/runs/$runid" - if ($wait) { - WaitWorkflow -repository $repository -runid $run.id - } - $run -} - -function DownloadWorkflowLog { - Param( - [string] $repository, - [string] $runid, - [string] $path - ) - - if (!$repository) { - $repository = $defaultRepository - } - RefreshToken -repository $repository - $headers = GetHeaders -token $ENV:GH_TOKEN -repository $repository - $url = "https://api.github.com/repos/$repository/actions/runs/$runid" - $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json) - $log = InvokeWebRequest -Method Get -Headers $headers -Uri $run.logs_url - $tempFileName = "$([System.IO.Path]::GetTempFileName()).zip" - [System.IO.File]::WriteAllBytes($tempFileName, $log.Content) - Expand-Archive -Path $tempFileName -DestinationPath $path -} - -function CancelAllWorkflows { - Param( - [string] $repository, - [switch] $noDelay - ) - if (-not $noDelay.IsPresent) { - Start-Sleep -Seconds 60 - } - $runs = invoke-gh api /repos/$repository/actions/runs -silent -returnValue | ConvertFrom-Json - foreach($run in $runs.workflow_runs) { - Write-Host $run.name - if ($run.status -eq 'in_progress') { - Write-Host "Cancelling $($run.name) run $($run.id)" - gh api --method POST /repos/$repository/actions/runs/$($run.id)/cancel | Out-Null - } - } -} - -function WaitAllWorkflows { - Param( - [string] $repository, - [switch] $noDelay, - [switch] $noError, - [int] $top = 999 - ) - if (-not $noDelay.IsPresent) { - Start-Sleep -Seconds 60 - } - $runs = invoke-gh api /repos/$repository/actions/runs -silent -returnValue | ConvertFrom-Json - $workflowRuns = $runs.workflow_runs | Select-Object -First $top - foreach($run in $workflowRuns) { - WaitWorkflow -repository $repository -runid $run.id -noDelay -noError:$noError - } -} - -function WaitWorkflow { - Param( - [string] $repository, - [string] $runid, - [switch] $noDelay, - [switch] $noError, - [switch] $noRerun - ) - - $delay = !$noDelay.IsPresent - if (!$repository) { - $repository = $defaultRepository - } - $status = "" - do { - RefreshToken -repository $repository - $headers = GetHeaders -token $ENV:GH_TOKEN -repository $repository - - if ($delay) { - Start-Sleep -Seconds 60 - } - WaitForRateLimit -headers $headers - $url = "https://api.github.com/repos/$repository/actions/runs/$runid" - $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json) - if ($run.status -ne $status) { - $status = $run.status - } - Write-Host "Workflow run is in status $status" - - $delay = $true - } while ($run.status -eq "queued" -or $run.status -eq "in_progress") - - Write-Host "Workflow conclusion: $($run.conclusion)" - - if ($run.conclusion -ne "Success" -and $run.conclusion -ne "cancelled") { - if (-not $noRerun.IsPresent) { - Write-Host "::warning::Rerunning workflow: $($run.name) run $($run.id), conclusion $($run.conclusion), url = $($run.html_url)" - invoke-gh api --method POST /repos/$repository/actions/runs/$runid/rerun | Out-Null - WaitWorkflow -repository $repository -runid $runid -noDelay:$noDelay -noError:$noError -noRerun - } - if (-not $noError.IsPresent) { throw "Workflow $($run.name), conclusion $($run.conclusion), url = $($run.html_url)" } - } -} - -function SetRepositorySecret { - Param( - [string] $repository, - [string] $name, - [string] $value - ) - - if (!$repository) { - $repository = $defaultRepository - } - Write-Host -ForegroundColor Yellow "`nSet Secret $name in $repository" - $value = $value.Replace("`r", '').Replace("`n", '') - gh secret set $name -b $value --repo $repository -} - -<# -.SYNOPSIS - Deletes all workflow runs in a repository. - -.DESCRIPTION - Cleans up workflow runs in a GitHub repository by deleting all existing runs. - This is useful for ensuring a clean state before running tests that track specific workflow runs. - -.PARAMETER repository - The full repository name in the format "owner/repo" (e.g., "microsoft/AL-Go"). - -.EXAMPLE - CleanupWorkflowRuns -repository "microsoft/AL-Go" -#> -function CleanupWorkflowRuns { - Param( - [Parameter(Mandatory = $true)] - [string] $repository - ) - - Write-Host -ForegroundColor Yellow "`nCleaning up workflow runs in $repository" - - RefreshToken -repository $repository - - # Get all workflow runs - $runs = invoke-gh api "/repos/$repository/actions/runs?per_page=100" -silent -returnValue | ConvertFrom-Json - - if ($runs.workflow_runs.Count -eq 0) { - Write-Host "No workflow runs found" - return - } - - Write-Host "Deleting $($runs.workflow_runs.Count) workflow runs..." - foreach ($run in $runs.workflow_runs) { - try { - Write-Host "Deleting run $($run.id) ($($run.name) - $($run.status))" - invoke-gh api /repos/$repository/actions/runs/$($run.id) --method DELETE -silent | Out-Null - } - catch { - Write-Host "Warning: Failed to delete run $($run.id): $_" - } - } - Write-Host "Cleanup completed" -} - -<# -.SYNOPSIS - Resets a repository to match the content of a source repository. - -.DESCRIPTION - Clones the target repository, fetches content from a source repository, and performs a hard reset - followed by a force push. This preserves the repository identity while resetting its content to - match the source repository. Useful for ensuring deterministic state in end-to-end tests. - -.PARAMETER repository - The full name of the target repository to reset in the format "owner/repo" (e.g., "microsoft/AL-Go"). - -.PARAMETER sourceRepository - The full name of the source repository to copy content from in the format "owner/repo". - -.PARAMETER branch - The branch name to reset. Defaults to "main". - -.EXAMPLE - ResetRepositoryToSource -repository "microsoft/test-repo" -sourceRepository "microsoft/source-repo" -branch "main" -#> -function ResetRepositoryToSource { - Param( - [Parameter(Mandatory = $true)] - [string] $repository, - [Parameter(Mandatory = $true)] - [string] $sourceRepository, - [string] $branch = "main" - ) - - Write-Host -ForegroundColor Yellow "`nResetting repository $repository to match $sourceRepository" - - RefreshToken -repository $repository - - # Clone the repository locally if not already in it - $tempPath = [System.IO.Path]::GetTempPath() - $repoPath = Join-Path $tempPath ([System.Guid]::NewGuid().ToString()) - New-Item $repoPath -ItemType Directory | Out-Null - - Push-Location $repoPath - try { - Write-Host "Cloning $repository..." - invoke-gh repo clone $repository . -- --quiet - if ($LASTEXITCODE -ne 0) { - throw "Failed to clone repository $repository" - } - - # Fetch the source repository content - Write-Host "Fetching source repository $sourceRepository..." - invoke-git remote add source "https://github.com/$sourceRepository.git" - if ($LASTEXITCODE -ne 0) { - throw "Failed to add remote source for $sourceRepository" - } - - invoke-git fetch source $branch --quiet - if ($LASTEXITCODE -ne 0) { - throw "Failed to fetch branch $branch from source $sourceRepository" - } - - # Reset the current branch to match the source - Write-Host "Resetting $branch to match source/$branch..." - invoke-git checkout $branch --quiet - if ($LASTEXITCODE -ne 0) { - throw "Failed to checkout branch $branch" - } - - invoke-git reset --hard "source/$branch" --quiet - if ($LASTEXITCODE -ne 0) { - throw "Failed to reset branch $branch to source/$branch" - } - - # Force push to update the repository - Write-Host "Force pushing changes..." - invoke-git push origin $branch --force --quiet - if ($LASTEXITCODE -ne 0) { - throw "Failed to push changes to $repository" - } - - Write-Host "Repository reset completed successfully" - } - catch { - Write-Host "Error resetting repository: $_" - throw - } - finally { - Pop-Location - Remove-Item -Path $repoPath -Force -Recurse -ErrorAction SilentlyContinue - } -} - -function CreateNewAppInFolder { - Param( - [string] $folder, - [string] $id = [GUID]::NewGUID().ToString(), - [int] $objID = 50000, - [string] $name, - [string] $publisher = $defaultPublisher, - [string] $version = "1.0.0.0", - [string] $application = $defaultApplication, - [string] $runtime = $defaultRuntime, - [HashTable[]] $dependencies = @() - ) - - $al = @( - "pageextension $objID ""CustListExt$name"" extends ""Customer List""" - "{" - " trigger OnOpenPage();" - " begin" - " Message('App published: Hello $name!');" - " end;" - "}") - $appJson = [ordered]@{ - "id" = $id - "name" = $name - "version" = $version - "publisher" = $publisher - "dependencies" = $dependencies - "application" = $application - "runtime" = $runtime - "idRanges" = @( @{ "from" = $objID; "to" = $objID } ) - "resourceExposurePolicy" = @{ "allowDebugging" = $true; "allowDownloadingSource" = $true; "includeSourceInSymbolFile" = $true } - } - $folder = Join-Path $folder $name - New-Item -Path $folder -ItemType Directory | Out-Null - $appJson | Set-JsonContentLF -Path (Join-Path $folder "app.json") - $al -join "`n" | Set-ContentLF -Path (Join-Path $folder "$name.al") - $id -} - -function ModifyAppInFolder { - Param( - [string] $folder, - [string] $name, - [string] $message = "Modify $name", - [switch] $commit, - [switch] $wait - ) - $alFile = Join-Path $folder "$name.al" - $al = Get-Content -Encoding utf8 -Path $alFile - # Add another ! to the end of the message and save - ($al -join "`n").Replace("!');","!!');") | Set-ContentLF -Path (Join-Path $folder "$name.al") - if ($commit) { - CommitAndPush -commitMessage $message -wait:$wait - } -} - -function CreateAlGoRepository { - Param( - [switch] $github, - [string] $repository, - [string] $template = "", - [string[]] $projects = @(), - [string] $contentPath, - [scriptBlock] $contentScript, - [switch] $private, - [switch] $linux, - [string] $branch = "main", - [hashtable] $addRepoSettings = @{} - ) - - if (!$repository) { - $repository = $defaultRepository - } - $waitMinutes = 0 - if ($github) { - $waitMinutes = Get-Random -Minimum 0 -Maximum 4 - } - $templateFolder = '' - if ($template.Contains('|')) { - # In order to run tests on the direct AL-Go Development branch, specify the folder in which the template is located after a | character in template - # example: "https://github.com/freddydk/AL-Go@branch|Templates/Per Tenant Extension" - $templateFolder = $template.Split('|')[1] - $templateOwner = $template.Split('/')[3] - $template = $template.Split('|')[0] - $waitMinutes = 0 # Do not wait when running tests on direct AL-Go Development branch - } - if (!$template.Contains('@')) { - $template += '@main' - } - $templateBranch = $template.Split('@')[1] - $templateRepo = $template.Split('@')[0] - - $tempPath = [System.IO.Path]::GetTempPath() - $path = Join-Path $tempPath ( [System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetTempFileName())) - New-Item $path -ItemType Directory | Out-Null - Set-Location $path - if ($waitMinutes) { - Write-Host "Waiting $waitMinutes minutes" - Start-Sleep -seconds ($waitMinutes*60) - } - if ($private) { - Write-Host -ForegroundColor Yellow "`nCreating private repository $repository (based on $template)" - invoke-gh repo create $repository --private --clone - } - else { - Write-Host -ForegroundColor Yellow "`nCreating public repository $repository (based on $template)" - invoke-gh repo create $repository --public --clone - } - Start-Sleep -seconds 10 - Set-Location '*' - - $templateUrl = "$templateRepo/archive/refs/heads/$templateBranch.zip" - Write-Host "Downloading template from $templateUrl" - $zipFileName = Join-Path $tempPath "$([GUID]::NewGuid().ToString()).zip" - [System.Net.WebClient]::new().DownloadFile($templateUrl, $zipFileName) - - $tempRepoPath = Join-Path $tempPath ([GUID]::NewGuid().ToString()) - Expand-Archive -Path $zipFileName -DestinationPath $tempRepoPath - Copy-Item (Join-Path (Get-Item "$tempRepoPath/*/$templateFolder").FullName '*') -Destination . -Recurse -Force - Remove-Item -Path $tempRepoPath -Force -Recurse - Remove-Item -Path $zipFileName -Force - if ($templateFolder) { - # This is a direct AL-Go development repository - # Replace URL's + references to microsoft/AL-Go-Actions with $templateOwner/AL-Go/Actions - Get-ChildItem -Path . -File -Recurse | ForEach-Object { - $file = $_.FullName - $lines = Get-Content -Encoding UTF8 -path $file - - # Replace URL's to actions repository first - $regex = "^(.*)https:\/\/raw\.githubusercontent\.com\/microsoft\/AL-Go-Actions\/main(.*)$" - $replace = "`${1}https://raw.githubusercontent.com/$($templateOwner)/AL-Go/$($templateBranch)/Actions`${2}" - $lines = $lines | ForEach-Object { $_ -replace $regex, $replace } - - # Replace AL-Go-Actions references - $regex = "^(.*)microsoft\/AL-Go-Actions(.*)main(.*)$" - $replace = "`${1}$($templateOwner)/AL-Go/Actions`${2}$($templateBranch)`${3}" - $lines = $lines | ForEach-Object { $_ -replace $regex, $replace } - - $content = "$($lines -join "`n")`n" - - # Update Template references in test apps - $content = $content.Replace('{TEMPLATEURL}', $template) - $content = $content.Replace('https://github.com/microsoft/AL-Go-PTE@main', $template) - $content = $content.Replace('https://github.com/microsoft/AL-Go-AppSource@main', $template) - - [System.IO.File]::WriteAllText($file, $content) - } - } - - if ($projects) { - # Make Repo multi-project - $projects | ForEach-Object { - New-Item $_ -ItemType Directory | Out-Null - Copy-Item '.AL-Go' -Destination $_ -Recurse -Force - } - Remove-Item '.AL-Go' -Force -Recurse - } - if ($contentPath) { - Write-Host "Copy content from $contentPath" - Copy-Item (Join-Path $contentPath "*") -Destination . -Recurse -Force - } - if ($contentScript) { - & $contentScript -path (get-location).Path - } - $repoSettingsFile = ".github\AL-Go-Settings.json" - $repoSettings = Get-Content $repoSettingsFile -Encoding UTF8 | ConvertFrom-Json - $runson = "windows-latest" - $shell = "powershell" - if ($linux) { - $runson = "ubuntu-latest" - $shell = "pwsh" - } - - if ($runson -ne "windows-latest" -or $shell -ne "powershell") { - $repoSettings | Add-Member -MemberType NoteProperty -Name "runs-on" -Value $runson - $repoSettings | Add-Member -MemberType NoteProperty -Name "shell" -Value $shell - Get-ChildItem -Path '.\.github\workflows\*.yaml' | ForEach-Object { - Write-Host $_.FullName - $content = Get-ContentLF -Path $_.FullName - $srcPattern = "runs-on: [ windows-latest ]`n" - $replacePattern = "runs-on: [ $runson ]`n" - $content = "$content`n".Replace($srcPattern, $replacePattern).TrimEnd("`n") - $srcPattern = "shell: powershell`n" - $replacePattern = "shell: $shell`n" - $content = "$content`n".Replace($srcPattern, $replacePattern).TrimEnd("`n") - [System.IO.File]::WriteAllText($_.FullName, $content) - } - } - # Disable telemetry AL-Go and BcContainerHelper telemetry when running end-2-end tests - $repoSettings | Add-Member -MemberType NoteProperty -Name "MicrosoftTelemetryConnectionString" -Value "" - $repoSettings | Set-JsonContentLF -path $repoSettingsFile - if ($addRepoSettings.Keys.Count) { - Add-PropertiesToJsonFile -path $repoSettingsFile -properties $addRepoSettings - } - - RefreshToken -repository $repository - - invoke-git add * - invoke-git commit --allow-empty -m 'init' - invoke-git branch -M $branch - if ($githubOwner) { - if ($github) { - invoke-git remote set-url origin "https://$($githubOwner)@github.com/$repository.git" - } else { - invoke-git remote set-url origin "https://github.com/$repository" - } - } - invoke-git push --set-upstream origin $branch - if (!$github) { - Start-Process "https://github.com/$repository/actions" - } - Start-Sleep -seconds 10 -} - -function Pull { - Param( - [string] $branch = "main" - ) - - invoke-git pull origin $branch -} - -function CommitAndPush { - Param( - [string] $commitMessage = "commitmessage", - [switch] $wait - ) - - if (!$repository) { - $repository = $defaultRepository - } - - RefreshToken -repository $repository - - if ($wait) { - $headers = GetHeaders -token $ENV:GH_TOKEN - Write-Host "Get Previous runs" - $url = "https://api.github.com/repos/$repository/actions/runs" - $previousrunids = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Select-Object -ExpandProperty id - if ($previousrunids) { - Write-Host "Previous runs: $($previousrunids -join ', ')" - } - else { - Write-Host "No previous runs found" - } - } - invoke-git add * - invoke-git commit --allow-empty -m "'$commitMessage'" - invoke-git push - if ($wait) { - while ($true) { - Start-Sleep -Seconds 10 - $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Where-Object { $previousrunids -notcontains $_.id } - if ($run) { - break - } - Write-Host "Run not started, waiting..." - } - WaitWorkflow -repository $repository -runid $run.id - $run - } -} - -function MergePRandPull { - Param( - [string] $repository, - [string] $branch = "main", - [switch] $wait - ) - - if (!$repository) { - $repository = $defaultRepository - } - - Write-Host "Get Previous runs" - RefreshToken -repository $repository - $headers = GetHeaders -token $ENV:GH_TOKEN -repository $repository - $url = "https://api.github.com/repos/$repository/actions/runs" - $previousrunids = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Select-Object -ExpandProperty id - if ($previousrunids) { - Write-Host "Previous runs: $($previousrunids -join ', ')" - } - else { - Write-Host "No previous runs found" - } - - $phs = @(invoke-gh -returnValue pr list --repo $repository) - if ($phs.Count -eq 0) { - throw "No Pull Request was created" - } - elseif ($phs.Count -gt 1) { - throw "More than one Pull Request exists" - } - $prid = $phs.Split("`t")[0] - Write-Host -ForegroundColor Yellow "`nMerge Pull Request $prid into repository $repository" - invoke-gh pr merge $prid --squash --delete-branch --repo $repository | Out-Host - while ($true) { - Start-Sleep -Seconds 10 - $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Where-Object { $previousrunids -notcontains $_.id } - if ($run) { - break - } - Write-Host "Run not started, waiting..." - } - if ($wait) { - WaitWorkflow -repository $repository -runid $run.id - } - Write-Host "Merge commit run: $($run.id)" - $run - Pull -branch $branch -} - -function RemoveRepository { - Param( - [string] $repository, - [string] $path = "" - ) - - if (!$repository) { - $repository = $defaultRepository - } - if ($repository) { - try { - $owner = $repository.Split("/")[0] - Write-Host -ForegroundColor Yellow "`nRemoving repository $repository" - # Remove all packages belonging to the repository - $ownerType = invoke-gh api users/$owner --jq .type -silent -returnValue - if ($ownerType -eq 'User') { - # Package belongs to a user - $ownerStr = "users/$owner" - } - else { - # Package belongs to an organization - $ownerStr = "orgs/$owner" - } - @((invoke-gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /$ownerStr/packages?package_type=nuget -silent -returnvalue -ErrorAction SilentlyContinue | ConvertFrom-Json)) | Where-Object { $_.PSObject.Properties.Name -eq 'repository' } | Where-Object { $_.repository.full_name -eq $repository } | ForEach-Object { - Write-Host "+ package $($_.name)" - # Pipe empty string into GH API --METHOD DELETE due to https://github.com/cli/cli/issues/3937 - '' | invoke-gh api --method DELETE -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /$ownerStr/packages/nuget/$($_.name) --input - } - } - catch { - Write-Host -ForegroundColor Red "Error removing packages" - Write-Host -ForegroundColor Red $_.Exception.Message - } - invoke-gh repo delete $repository --yes | Out-Host - } - - if ($path) { - if (-not $path.StartsWith("$([System.IO.Path]::GetTempPath())",[StringComparison]::InvariantCultureIgnoreCase)) { - throw "$path is not temppath" - } - else { - Set-Location ([System.IO.Path]::GetTempPath()) - Remove-Item $path -Recurse -Force - } - } -} - -function Test-LogContainsFromRun { - Param( - [string] $repository, - [string] $runid, - [string] $jobName, - [string] $stepName, - [string] $expectedText, - [switch] $isRegEx - ) - - DownloadWorkflowLog -repository $repository -runid $runid -path 'logs' - try { - # Log format changes are rolling out on GitHub Actions, we have to support both - $oldStepLogFile = "logs/$jobName/*_$stepName.txt" - $newJobLogFile = "logs/*_$jobName.txt" - if (Test-Path -Path $oldStepLogFile) { - $runPipelineLog = Get-Content -Path (Get-Item $oldStepLogFile).FullName -encoding utf8 -raw - } - else { - $jobLog = Get-Content -Path (Get-Item $newJobLogFile).FullName -encoding utf8 - $emit = $false - $runPipelineLog = @($jobLog | ForEach-Object { - if ($emit -and $_ -like "*##[[]group]Run *@*") { - Write-Host -ForegroundColor Yellow "Foundend $_" - $emit = $false - } - elseif ($_ -like "*##[[]group]Run *$StepName@*") { - Write-Host -ForegroundColor Yellow "Foundstart $_" - $emit = $true - } - else { - Write-Host -ForegroundColor Gray $_ - } - if ($emit) { $_ } - }) -join "`n" - } - - if ($isRegEx) { - $found = $runPipelineLog -match $expectedText - return $Matches - } - else { - $found = $runPipelineLog.indexOf($expectedText, [System.StringComparison]::OrdinalIgnoreCase) -ne -1 - } - - if ($found) { - Write-Host "'$expectedText' found in the log for '$jobName -> $stepName' as expected" - } - else { - throw "Expected to find '$expectedText' in the log for '$jobName -> $stepName', but did not find it" - } - } - finally { - Remove-Item -Path 'logs' -Recurse -Force - } -} - -. (Join-Path $PSScriptRoot "Workflows\RunAddExistingAppOrTestApp.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunCICD.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunCreateApp.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunDeployReferenceDocumentation.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunCreateOnlineDevelopmentEnvironment.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunCreateRelease.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunCreateTestApp.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunIncrementVersionNumber.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunPublishToEnvironment.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunUpdateAlGoSystemFiles.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunTestCurrent.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunTestNextMinor.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunTestNextMajor.ps1") - -. (Join-Path $PSScriptRoot "Test-Functions.ps1") +$githubOwner = "githubOwner" +$token = "DefaultToken" +$defaultRepository = "repo" +$defaultApplication = "22.0.0.0" +$defaultRuntime = "10.0" +$defaultPublisher = "MS Test" +$lastTokenRefresh = 0 + +Import-Module (Join-Path $PSScriptRoot "..\Actions\Github-Helper.psm1" -Resolve) -DisableNameChecking -Global +. (Join-Path $PSScriptRoot "..\Actions\AL-Go-Helper.ps1" -Resolve) + +function GetDefaultPublisher() { + return $defaultPublisher +} + +function SetTokenAndRepository { + Param( + [string] $githubOwner, + [string] $token, + [string] $appId, + [string] $appKey, + [string] $repository, + [switch] $github + ) + + $script:githubOwner = $githubOwner + $script:defaultRepository = $repository + + if ($github) { + invoke-git config --global user.email "$githubOwner@users.noreply.github.com" + invoke-git config --global user.name "$githubOwner" + invoke-git config --global hub.protocol https + invoke-git config --global core.autocrlf false + } + + if (-not $github) { + # Running locally - Ensure the user is authenticated with the GitHub CLI. + # This is required for local runs to perform GitHub-related operations. + invoke-gh auth status + gh auth refresh --scopes repo,admin:org,workflow,write:packages,read:packages,delete:packages,user,delete_repo + } elseif ($appKey -and $appId) { + # Running in GitHub Actions + $token = @{ "GitHubAppClientId" = $appId; "PrivateKey" = ($appKey -join '') } | ConvertTo-Json -Compress -Depth 99 + } else { + throw "GitHub App ID and Private Key not set. In order to run end to end tests, you need a Secret called E2E_PRIVATE_KEY and a variable called E2E_APP_ID." + } + + # Repository isn't created yet so authenticating towards the .github repository + RefreshToken -token $token -repository "$githubOwner/.github" +} + +function RefreshToken { + Param( + [Parameter(Mandatory = $false)] + [string] $token, + [Parameter(Mandatory = $true)] + [string] $repository, + [Parameter(Mandatory = $false)] + [switch] $force + ) + if ($github) { + if ($token) { + $script:token = $token + } + + if ($script:token -eq "DefaultToken") { + throw "Token not set." + } + + # Check if the last token refresh was more than 10 minutes ago + + if ((-not $force) -and ($script:lastTokenRefresh -ne 0) -and (([DateTime]::Now - $script:lastTokenRefresh).TotalMinutes -lt 10)) { + return + } + + Write-Host "Authenticating with GitHub using token" + $realToken = GetAccessToken -token $script:token -repository $repository -repositories @() + $script:lastTokenRefresh = [DateTime]::Now + $ENV:GITHUB_TOKEN = $realToken + $ENV:GH_TOKEN = $realToken + invoke-gh auth setup-git # Use GitHub CLI as a credential helper + } else { + $realToken = gh auth token + $ENV:GITHUB_TOKEN = $realToken + $ENV:GH_TOKEN = $realToken + invoke-gh auth setup-git # Use GitHub CLI as a credential helper + } +} + +function Add-PropertiesToJsonFile { + Param( + [string] $path, + [hashTable] $properties, + [switch] $commit, + [switch] $wait + ) + + Write-Host -ForegroundColor Yellow "`nAdd Properties to $([System.IO.Path]::GetFileName($path))" + Write-Host "Properties" + $properties | Out-Host + + $json = Get-Content $path -Encoding UTF8 | ConvertFrom-Json | ConvertTo-HashTable -recurse + $properties.Keys | ForEach-Object { + $json."$_" = $properties."$_" + } + $json | Set-JsonContentLF -path $path + + if ($commit) { + CommitAndPush -commitMessage "Add properties to $([System.IO.Path]::GetFileName($path))" -wait:$wait + } +} + +function Remove-PropertiesFromJsonFile { + Param( + [string] $path, + [string[]] $properties, + [switch] $commit, + [switch] $wait + ) + + Write-Host -ForegroundColor Yellow "`nRemove Properties from $([System.IO.Path]::GetFileName($path))" + Write-Host "Properties" + $properties | Out-Host + + $json = Get-Content $path -Encoding UTF8 | ConvertFrom-Json | ConvertTo-HashTable -recurse + $keys = @($json.Keys) + $keys | ForEach-Object { + $key = $_ + if ($properties | Where-Object { $key -like $_ }) { + $json.Remove($key) + } + } + $json | Set-JsonContentLF -path $path + + if ($commit) { + CommitAndPush -commitMessage "Remove properties from $([System.IO.Path]::GetFileName($path))" -wait:$wait + } +} + +function RunWorkflow { + Param( + [string] $name, + [hashtable] $parameters = @{}, + [switch] $wait, + [string] $repository, + [string] $branch = "main" + ) + + if (!$repository) { + $repository = $defaultRepository + } + Write-Host -ForegroundColor Yellow "`nRun workflow $($name.Trim()) in $repository" + if ($parameters -and $parameters.Count -gt 0) { + Write-Host "Parameters:" + Write-Host ($parameters | ConvertTo-Json) + } + + RefreshToken -repository $repository + + $headers = GetHeaders -token $Env:GH_TOKEN -repository $repository + WaitForRateLimit -headers $headers -displayStatus + + Write-Host "Get Workflows" + $url = "https://api.github.com/repos/$repository/actions/workflows" + $workflows = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflows + $workflows | ForEach-Object { Write-Host "- $($_.Name)"} + if (!$workflows) { + Write-Host "No workflows found, waiting 60 seconds and retrying" + Start-Sleep -seconds 60 + $workflows = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflows + $workflows | ForEach-Object { Write-Host "- $($_.Name)"} + if (!$workflows) { + throw "No workflows found" + } + } + $workflow = $workflows | Where-Object { $_.Name.Trim() -eq $name } + if (!$workflow) { + throw "Workflow $name not found" + } + + Write-Host "Get Previous runs" + $url = "https://api.github.com/repos/$repository/actions/runs" + $previousrun = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.workflow_id -eq $workflow.id -and $_.event -eq 'workflow_dispatch' } | Select-Object -First 1 + if ($previousrun) { + Write-Host "Previous run: $($previousrun.id)" + } + else { + Write-Host "No previous run found" + } + + Write-Host "Run workflow" + $url = "https://api.github.com/repos/$repository/actions/workflows/$($workflow.id)/dispatches" + Write-Host $url + $body = @{ + "ref" = "refs/heads/$branch" + "inputs" = $parameters + } + InvokeWebRequest -Method Post -Headers $headers -Uri $url -Body ($body | ConvertTo-Json) | Out-Null + + Write-Host "Queuing" + do { + Start-Sleep -Seconds 10 + $url = "https://api.github.com/repos/$repository/actions/runs" + $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.workflow_id -eq $workflow.id -and $_.event -eq 'workflow_dispatch' } | Select-Object -First 1 + Write-Host "." + } until (($run) -and ((!$previousrun) -or ($run.id -ne $previousrun.id))) + $runid = $run.id + Write-Host "Run URL: https://github.com/$repository/actions/runs/$runid" + if ($wait) { + WaitWorkflow -repository $repository -runid $run.id + } + $run +} + +function DownloadWorkflowLog { + Param( + [string] $repository, + [string] $runid, + [string] $path + ) + + if (!$repository) { + $repository = $defaultRepository + } + RefreshToken -repository $repository + $headers = GetHeaders -token $ENV:GH_TOKEN -repository $repository + $url = "https://api.github.com/repos/$repository/actions/runs/$runid" + $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json) + $log = InvokeWebRequest -Method Get -Headers $headers -Uri $run.logs_url + $tempFileName = "$([System.IO.Path]::GetTempFileName()).zip" + [System.IO.File]::WriteAllBytes($tempFileName, $log.Content) + Expand-Archive -Path $tempFileName -DestinationPath $path +} + +function CancelAllWorkflows { + Param( + [string] $repository, + [switch] $noDelay + ) + if (-not $noDelay.IsPresent) { + Start-Sleep -Seconds 60 + } + $runs = invoke-gh api /repos/$repository/actions/runs -silent -returnValue | ConvertFrom-Json + foreach($run in $runs.workflow_runs) { + Write-Host $run.name + if ($run.status -eq 'in_progress') { + Write-Host "Cancelling $($run.name) run $($run.id)" + gh api --method POST /repos/$repository/actions/runs/$($run.id)/cancel | Out-Null + } + } +} + +function WaitAllWorkflows { + Param( + [string] $repository, + [switch] $noDelay, + [switch] $noError, + [int] $top = 999 + ) + if (-not $noDelay.IsPresent) { + Start-Sleep -Seconds 60 + } + $runs = invoke-gh api /repos/$repository/actions/runs -silent -returnValue | ConvertFrom-Json + $workflowRuns = $runs.workflow_runs | Select-Object -First $top + foreach($run in $workflowRuns) { + WaitWorkflow -repository $repository -runid $run.id -noDelay -noError:$noError + } +} + +function WaitWorkflow { + Param( + [string] $repository, + [string] $runid, + [switch] $noDelay, + [switch] $noError, + [switch] $noRerun + ) + + $delay = !$noDelay.IsPresent + if (!$repository) { + $repository = $defaultRepository + } + $status = "" + do { + RefreshToken -repository $repository + $headers = GetHeaders -token $ENV:GH_TOKEN -repository $repository + + if ($delay) { + Start-Sleep -Seconds 60 + } + WaitForRateLimit -headers $headers + $url = "https://api.github.com/repos/$repository/actions/runs/$runid" + $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json) + if ($run.status -ne $status) { + $status = $run.status + } + Write-Host "Workflow run is in status $status" + + $delay = $true + } while ($run.status -eq "queued" -or $run.status -eq "in_progress") + + Write-Host "Workflow conclusion: $($run.conclusion)" + + if ($run.conclusion -ne "Success" -and $run.conclusion -ne "cancelled") { + if (-not $noRerun.IsPresent) { + Write-Host "::warning::Rerunning workflow: $($run.name) run $($run.id), conclusion $($run.conclusion), url = $($run.html_url)" + invoke-gh api --method POST /repos/$repository/actions/runs/$runid/rerun | Out-Null + WaitWorkflow -repository $repository -runid $runid -noDelay:$noDelay -noError:$noError -noRerun + } + if (-not $noError.IsPresent) { throw "Workflow $($run.name), conclusion $($run.conclusion), url = $($run.html_url)" } + } +} + +function SetRepositorySecret { + Param( + [string] $repository, + [string] $name, + [string] $value + ) + + if (!$repository) { + $repository = $defaultRepository + } + Write-Host -ForegroundColor Yellow "`nSet Secret $name in $repository" + $value = $value.Replace("`r", '').Replace("`n", '') + gh secret set $name -b $value --repo $repository +} + +<# +.SYNOPSIS + Deletes all workflow runs in a repository. + +.DESCRIPTION + Cleans up workflow runs in a GitHub repository by deleting all existing runs. + This is useful for ensuring a clean state before running tests that track specific workflow runs. + +.PARAMETER repository + The full repository name in the format "owner/repo" (e.g., "microsoft/AL-Go"). + +.EXAMPLE + CleanupWorkflowRuns -repository "microsoft/AL-Go" +#> +function CleanupWorkflowRuns { + Param( + [Parameter(Mandatory = $true)] + [string] $repository + ) + + Write-Host -ForegroundColor Yellow "`nCleaning up workflow runs in $repository" + + RefreshToken -repository $repository + + # Get all workflow runs + $runs = invoke-gh api "/repos/$repository/actions/runs?per_page=100" -silent -returnValue | ConvertFrom-Json + + if ($runs.workflow_runs.Count -eq 0) { + Write-Host "No workflow runs found" + return + } + + Write-Host "Deleting $($runs.workflow_runs.Count) workflow runs..." + foreach ($run in $runs.workflow_runs) { + try { + Write-Host "Deleting run $($run.id) ($($run.name) - $($run.status))" + invoke-gh api /repos/$repository/actions/runs/$($run.id) --method DELETE -silent | Out-Null + } + catch { + Write-Host "Warning: Failed to delete run $($run.id): $_" + } + } + Write-Host "Cleanup completed" +} + +<# +.SYNOPSIS + Resets a repository to match the content of a source repository. + +.DESCRIPTION + Clones the target repository, fetches content from a source repository, and performs a hard reset + followed by a force push. This preserves the repository identity while resetting its content to + match the source repository. Useful for ensuring deterministic state in end-to-end tests. + +.PARAMETER repository + The full name of the target repository to reset in the format "owner/repo" (e.g., "microsoft/AL-Go"). + +.PARAMETER sourceRepository + The full name of the source repository to copy content from in the format "owner/repo". + +.PARAMETER branch + The branch name to reset. Defaults to "main". + +.EXAMPLE + ResetRepositoryToSource -repository "microsoft/test-repo" -sourceRepository "microsoft/source-repo" -branch "main" +#> +function ResetRepositoryToSource { + Param( + [Parameter(Mandatory = $true)] + [string] $repository, + [Parameter(Mandatory = $true)] + [string] $sourceRepository, + [string] $branch = "main" + ) + + Write-Host -ForegroundColor Yellow "`nResetting repository $repository to match $sourceRepository" + + RefreshToken -repository $repository + + # Clone the repository locally if not already in it + $tempPath = [System.IO.Path]::GetTempPath() + $repoPath = Join-Path $tempPath ([System.Guid]::NewGuid().ToString()) + New-Item $repoPath -ItemType Directory | Out-Null + + Push-Location $repoPath + try { + Write-Host "Cloning $repository..." + invoke-gh repo clone $repository . -- --quiet + if ($LASTEXITCODE -ne 0) { + throw "Failed to clone repository $repository" + } + + # Fetch the source repository content + Write-Host "Fetching source repository $sourceRepository..." + invoke-git remote add source "https://github.com/$sourceRepository.git" + if ($LASTEXITCODE -ne 0) { + throw "Failed to add remote source for $sourceRepository" + } + + invoke-git fetch source $branch --quiet + if ($LASTEXITCODE -ne 0) { + throw "Failed to fetch branch $branch from source $sourceRepository" + } + + # Reset the current branch to match the source + Write-Host "Resetting $branch to match source/$branch..." + invoke-git checkout $branch --quiet + if ($LASTEXITCODE -ne 0) { + throw "Failed to checkout branch $branch" + } + + invoke-git reset --hard "source/$branch" --quiet + if ($LASTEXITCODE -ne 0) { + throw "Failed to reset branch $branch to source/$branch" + } + + # Force push to update the repository + Write-Host "Force pushing changes..." + invoke-git push origin $branch --force --quiet + if ($LASTEXITCODE -ne 0) { + throw "Failed to push changes to $repository" + } + + Write-Host "Repository reset completed successfully" + } + catch { + Write-Host "Error resetting repository: $_" + throw + } + finally { + Pop-Location + Remove-Item -Path $repoPath -Force -Recurse -ErrorAction SilentlyContinue + } +} + +function CreateNewAppInFolder { + Param( + [string] $folder, + [string] $id = [GUID]::NewGUID().ToString(), + [int] $objID = 50000, + [string] $name, + [string] $publisher = $defaultPublisher, + [string] $version = "1.0.0.0", + [string] $application = $defaultApplication, + [string] $runtime = $defaultRuntime, + [HashTable[]] $dependencies = @() + ) + + $al = @( + "pageextension $objID ""CustListExt$name"" extends ""Customer List""" + "{" + " trigger OnOpenPage();" + " begin" + " Message('App published: Hello $name!');" + " end;" + "}") + $appJson = [ordered]@{ + "id" = $id + "name" = $name + "version" = $version + "publisher" = $publisher + "dependencies" = $dependencies + "application" = $application + "runtime" = $runtime + "idRanges" = @( @{ "from" = $objID; "to" = $objID } ) + "resourceExposurePolicy" = @{ "allowDebugging" = $true; "allowDownloadingSource" = $true; "includeSourceInSymbolFile" = $true } + } + $folder = Join-Path $folder $name + New-Item -Path $folder -ItemType Directory | Out-Null + $appJson | Set-JsonContentLF -Path (Join-Path $folder "app.json") + $al -join "`n" | Set-ContentLF -Path (Join-Path $folder "$name.al") + $id +} + +function ModifyAppInFolder { + Param( + [string] $folder, + [string] $name, + [string] $message = "Modify $name", + [switch] $commit, + [switch] $wait + ) + $alFile = Join-Path $folder "$name.al" + $al = Get-Content -Encoding utf8 -Path $alFile + # Add another ! to the end of the message and save + ($al -join "`n").Replace("!');","!!');") | Set-ContentLF -Path (Join-Path $folder "$name.al") + if ($commit) { + CommitAndPush -commitMessage $message -wait:$wait + } +} + +function CreateAlGoRepository { + Param( + [switch] $github, + [string] $repository, + [string] $template = "", + [string[]] $projects = @(), + [string] $contentPath, + [scriptBlock] $contentScript, + [switch] $private, + [switch] $linux, + [string] $branch = "main", + [hashtable] $addRepoSettings = @{} + ) + + if (!$repository) { + $repository = $defaultRepository + } + $waitMinutes = 0 + if ($github) { + $waitMinutes = Get-Random -Minimum 0 -Maximum 4 + } + $templateFolder = '' + if ($template.Contains('|')) { + # In order to run tests on the direct AL-Go Development branch, specify the folder in which the template is located after a | character in template + # example: "https://github.com/freddydk/AL-Go@branch|Templates/Per Tenant Extension" + $templateFolder = $template.Split('|')[1] + $templateOwner = $template.Split('/')[3] + $template = $template.Split('|')[0] + $waitMinutes = 0 # Do not wait when running tests on direct AL-Go Development branch + } + if (!$template.Contains('@')) { + $template += '@main' + } + $templateBranch = $template.Split('@')[1] + $templateRepo = $template.Split('@')[0] + + $tempPath = [System.IO.Path]::GetTempPath() + $path = Join-Path $tempPath ( [System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetTempFileName())) + New-Item $path -ItemType Directory | Out-Null + Set-Location $path + if ($waitMinutes) { + Write-Host "Waiting $waitMinutes minutes" + Start-Sleep -seconds ($waitMinutes*60) + } + if ($private) { + Write-Host -ForegroundColor Yellow "`nCreating private repository $repository (based on $template)" + invoke-gh repo create $repository --private --clone + } + else { + Write-Host -ForegroundColor Yellow "`nCreating public repository $repository (based on $template)" + invoke-gh repo create $repository --public --clone + } + Start-Sleep -seconds 10 + Set-Location '*' + + $templateUrl = "$templateRepo/archive/refs/heads/$templateBranch.zip" + Write-Host "Downloading template from $templateUrl" + $zipFileName = Join-Path $tempPath "$([GUID]::NewGuid().ToString()).zip" + [System.Net.WebClient]::new().DownloadFile($templateUrl, $zipFileName) + + $tempRepoPath = Join-Path $tempPath ([GUID]::NewGuid().ToString()) + Expand-Archive -Path $zipFileName -DestinationPath $tempRepoPath + Copy-Item (Join-Path (Get-Item "$tempRepoPath/*/$templateFolder").FullName '*') -Destination . -Recurse -Force + Remove-Item -Path $tempRepoPath -Force -Recurse + Remove-Item -Path $zipFileName -Force + if ($templateFolder) { + # This is a direct AL-Go development repository + # Replace URL's + references to microsoft/AL-Go-Actions with $templateOwner/AL-Go/Actions + Get-ChildItem -Path . -File -Recurse | ForEach-Object { + $file = $_.FullName + $lines = Get-Content -Encoding UTF8 -path $file + + # Replace URL's to actions repository first + $regex = "^(.*)https:\/\/raw\.githubusercontent\.com\/microsoft\/AL-Go-Actions\/main(.*)$" + $replace = "`${1}https://raw.githubusercontent.com/$($templateOwner)/AL-Go/$($templateBranch)/Actions`${2}" + $lines = $lines | ForEach-Object { $_ -replace $regex, $replace } + + # Replace AL-Go-Actions references + $regex = "^(.*)microsoft\/AL-Go-Actions(.*)main(.*)$" + $replace = "`${1}$($templateOwner)/AL-Go/Actions`${2}$($templateBranch)`${3}" + $lines = $lines | ForEach-Object { $_ -replace $regex, $replace } + + $content = "$($lines -join "`n")`n" + + # Update Template references in test apps + $content = $content.Replace('{TEMPLATEURL}', $template) + $content = $content.Replace('https://github.com/microsoft/AL-Go-PTE@main', $template) + $content = $content.Replace('https://github.com/microsoft/AL-Go-AppSource@main', $template) + + [System.IO.File]::WriteAllText($file, $content) + } + } + + if ($projects) { + # Make Repo multi-project + $projects | ForEach-Object { + New-Item $_ -ItemType Directory | Out-Null + Copy-Item '.AL-Go' -Destination $_ -Recurse -Force + } + Remove-Item '.AL-Go' -Force -Recurse + } + if ($contentPath) { + Write-Host "Copy content from $contentPath" + Copy-Item (Join-Path $contentPath "*") -Destination . -Recurse -Force + } + if ($contentScript) { + & $contentScript -path (get-location).Path + } + $repoSettingsFile = ".github\AL-Go-Settings.json" + $repoSettings = Get-Content $repoSettingsFile -Encoding UTF8 | ConvertFrom-Json + $runson = "windows-latest" + $shell = "powershell" + if ($linux) { + $runson = "ubuntu-latest" + $shell = "pwsh" + } + + if ($runson -ne "windows-latest" -or $shell -ne "powershell") { + $repoSettings | Add-Member -MemberType NoteProperty -Name "runs-on" -Value $runson + $repoSettings | Add-Member -MemberType NoteProperty -Name "shell" -Value $shell + Get-ChildItem -Path '.\.github\workflows\*.yaml' | ForEach-Object { + Write-Host $_.FullName + $content = Get-ContentLF -Path $_.FullName + $srcPattern = "runs-on: [ windows-latest ]`n" + $replacePattern = "runs-on: [ $runson ]`n" + $content = "$content`n".Replace($srcPattern, $replacePattern).TrimEnd("`n") + $srcPattern = "shell: powershell`n" + $replacePattern = "shell: $shell`n" + $content = "$content`n".Replace($srcPattern, $replacePattern).TrimEnd("`n") + [System.IO.File]::WriteAllText($_.FullName, $content) + } + } + # Disable telemetry AL-Go and BcContainerHelper telemetry when running end-2-end tests + $repoSettings | Add-Member -MemberType NoteProperty -Name "MicrosoftTelemetryConnectionString" -Value "" + $repoSettings | Set-JsonContentLF -path $repoSettingsFile + if ($addRepoSettings.Keys.Count) { + Add-PropertiesToJsonFile -path $repoSettingsFile -properties $addRepoSettings + } + + RefreshToken -repository $repository + + invoke-git add * + invoke-git commit --allow-empty -m 'init' + invoke-git branch -M $branch + if ($githubOwner) { + if ($github) { + invoke-git remote set-url origin "https://$($githubOwner)@github.com/$repository.git" + } else { + invoke-git remote set-url origin "https://github.com/$repository" + } + } + invoke-git push --set-upstream origin $branch + if (!$github) { + Start-Process "https://github.com/$repository/actions" + } + Start-Sleep -seconds 10 +} + +function Pull { + Param( + [string] $branch = "main" + ) + + invoke-git pull origin $branch +} + +function CommitAndPush { + Param( + [string] $commitMessage = "commitmessage", + [switch] $wait + ) + + if (!$repository) { + $repository = $defaultRepository + } + + RefreshToken -repository $repository + + if ($wait) { + $headers = GetHeaders -token $ENV:GH_TOKEN + Write-Host "Get Previous runs" + $url = "https://api.github.com/repos/$repository/actions/runs" + $previousrunids = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Select-Object -ExpandProperty id + if ($previousrunids) { + Write-Host "Previous runs: $($previousrunids -join ', ')" + } + else { + Write-Host "No previous runs found" + } + } + invoke-git add * + invoke-git commit --allow-empty -m "'$commitMessage'" + invoke-git push + if ($wait) { + while ($true) { + Start-Sleep -Seconds 10 + $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Where-Object { $previousrunids -notcontains $_.id } + if ($run) { + break + } + Write-Host "Run not started, waiting..." + } + WaitWorkflow -repository $repository -runid $run.id + $run + } +} + +function MergePRandPull { + Param( + [string] $repository, + [string] $branch = "main", + [switch] $wait + ) + + if (!$repository) { + $repository = $defaultRepository + } + + Write-Host "Get Previous runs" + RefreshToken -repository $repository + $headers = GetHeaders -token $ENV:GH_TOKEN -repository $repository + $url = "https://api.github.com/repos/$repository/actions/runs" + $previousrunids = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Select-Object -ExpandProperty id + if ($previousrunids) { + Write-Host "Previous runs: $($previousrunids -join ', ')" + } + else { + Write-Host "No previous runs found" + } + + $phs = @(invoke-gh -returnValue pr list --repo $repository) + if ($phs.Count -eq 0) { + throw "No Pull Request was created" + } + elseif ($phs.Count -gt 1) { + throw "More than one Pull Request exists" + } + $prid = $phs.Split("`t")[0] + Write-Host -ForegroundColor Yellow "`nMerge Pull Request $prid into repository $repository" + invoke-gh pr merge $prid --squash --delete-branch --repo $repository | Out-Host + while ($true) { + Start-Sleep -Seconds 10 + $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Where-Object { $previousrunids -notcontains $_.id } + if ($run) { + break + } + Write-Host "Run not started, waiting..." + } + if ($wait) { + WaitWorkflow -repository $repository -runid $run.id + } + Write-Host "Merge commit run: $($run.id)" + $run + Pull -branch $branch +} + +function RemoveRepository { + Param( + [string] $repository, + [string] $path = "" + ) + + if (!$repository) { + $repository = $defaultRepository + } + if ($repository) { + try { + $owner = $repository.Split("/")[0] + Write-Host -ForegroundColor Yellow "`nRemoving repository $repository" + # Remove all packages belonging to the repository + $ownerType = invoke-gh api users/$owner --jq .type -silent -returnValue + if ($ownerType -eq 'User') { + # Package belongs to a user + $ownerStr = "users/$owner" + } + else { + # Package belongs to an organization + $ownerStr = "orgs/$owner" + } + @((invoke-gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /$ownerStr/packages?package_type=nuget -silent -returnvalue -ErrorAction SilentlyContinue | ConvertFrom-Json)) | Where-Object { $_.PSObject.Properties.Name -eq 'repository' } | Where-Object { $_.repository.full_name -eq $repository } | ForEach-Object { + Write-Host "+ package $($_.name)" + # Pipe empty string into GH API --METHOD DELETE due to https://github.com/cli/cli/issues/3937 + '' | invoke-gh api --method DELETE -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /$ownerStr/packages/nuget/$($_.name) --input + } + } + catch { + Write-Host -ForegroundColor Red "Error removing packages" + Write-Host -ForegroundColor Red $_.Exception.Message + } + invoke-gh repo delete $repository --yes | Out-Host + } + + if ($path) { + if (-not $path.StartsWith("$([System.IO.Path]::GetTempPath())",[StringComparison]::InvariantCultureIgnoreCase)) { + throw "$path is not temppath" + } + else { + Set-Location ([System.IO.Path]::GetTempPath()) + Remove-Item $path -Recurse -Force + } + } +} + +function Test-LogContainsFromRun { + Param( + [string] $repository, + [string] $runid, + [string] $jobName, + [string] $stepName, + [string] $expectedText, + [switch] $isRegEx + ) + + DownloadWorkflowLog -repository $repository -runid $runid -path 'logs' + try { + # Log format changes are rolling out on GitHub Actions, we have to support both + $oldStepLogFile = "logs/$jobName/*_$stepName.txt" + $newJobLogFile = "logs/*_$jobName.txt" + if (Test-Path -Path $oldStepLogFile) { + $runPipelineLog = Get-Content -Path (Get-Item $oldStepLogFile).FullName -encoding utf8 -raw + } + else { + $jobLog = Get-Content -Path (Get-Item $newJobLogFile).FullName -encoding utf8 + $emit = $false + $runPipelineLog = @($jobLog | ForEach-Object { + if ($emit -and $_ -like "*##[[]group]Run *@*") { + Write-Host -ForegroundColor Yellow "Foundend $_" + $emit = $false + } + elseif ($_ -like "*##[[]group]Run *$StepName@*") { + Write-Host -ForegroundColor Yellow "Foundstart $_" + $emit = $true + } + else { + Write-Host -ForegroundColor Gray $_ + } + if ($emit) { $_ } + }) -join "`n" + } + + if ($isRegEx) { + $found = $runPipelineLog -match $expectedText + return $Matches + } + else { + $found = $runPipelineLog.indexOf($expectedText, [System.StringComparison]::OrdinalIgnoreCase) -ne -1 + } + + if ($found) { + Write-Host "'$expectedText' found in the log for '$jobName -> $stepName' as expected" + } + else { + throw "Expected to find '$expectedText' in the log for '$jobName -> $stepName', but did not find it" + } + } + finally { + Remove-Item -Path 'logs' -Recurse -Force + } +} + +. (Join-Path $PSScriptRoot "Workflows\RunAddExistingAppOrTestApp.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunCICD.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunCreateApp.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunDeployReferenceDocumentation.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunCreateOnlineDevelopmentEnvironment.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunCreateRelease.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunCreateTestApp.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunIncrementVersionNumber.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunPublishToEnvironment.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunUpdateAlGoSystemFiles.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunTestCurrent.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunTestNextMinor.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunTestNextMajor.ps1") + +. (Join-Path $PSScriptRoot "Test-Functions.ps1") From ab19c26f882c90c8ad7c13b671d4f2051f7e33e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:25:01 +0000 Subject: [PATCH 10/12] Fix 'unknown flag: --quiet' error by removing unsupported flag from gh repo clone Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- e2eTests/e2eTestHelper.psm1 | 2 +- e2eTests/scenarios/FederatedCredentials/runtest.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/e2eTests/e2eTestHelper.psm1 b/e2eTests/e2eTestHelper.psm1 index c62c724afe..eead106734 100644 --- a/e2eTests/e2eTestHelper.psm1 +++ b/e2eTests/e2eTestHelper.psm1 @@ -413,7 +413,7 @@ function ResetRepositoryToSource { Push-Location $repoPath try { Write-Host "Cloning $repository..." - invoke-gh repo clone $repository . -- --quiet + invoke-gh repo clone $repository . if ($LASTEXITCODE -ne 0) { throw "Failed to clone repository $repository" } diff --git a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 index f84399e02b..3c86b09abc 100644 --- a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 +++ b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 @@ -95,7 +95,7 @@ New-Item $repoPath -ItemType Directory | Out-Null Push-Location $repoPath try { Write-Host "Re-applying repository settings..." - invoke-gh repo clone $repository . -- --quiet + invoke-gh repo clone $repository . $repoSettingsFile = ".github\AL-Go-Settings.json" if (Test-Path $repoSettingsFile) { Add-PropertiesToJsonFile -path $repoSettingsFile -properties @{"ghTokenWorkflowSecretName" = "e2eghTokenWorkflow"} From 44d1b6cf130ad09021e4a62502f930b4d720aab6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:43:08 +0000 Subject: [PATCH 11/12] Merge main branch and resolve conflicts in e2eTestHelper.psm1 Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- .github/workflows/powershell.yaml | 2 +- .github/workflows/scorecard-analysis.yml | 2 +- .../.github/workflows/_BuildALGoProject.yaml | 2 +- .../.github/workflows/_BuildALGoProject.yaml | 2 +- e2eTests/e2eTestHelper.psm1 | 1790 +++++++++-------- 5 files changed, 900 insertions(+), 898 deletions(-) diff --git a/.github/workflows/powershell.yaml b/.github/workflows/powershell.yaml index 51c370c089..af20dd8614 100644 --- a/.github/workflows/powershell.yaml +++ b/.github/workflows/powershell.yaml @@ -38,6 +38,6 @@ jobs: # Upload the SARIF file generated in the previous step - name: Upload SARIF results file - uses: github/codeql-action/upload-sarif@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 + uses: github/codeql-action/upload-sarif@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 with: sarif_file: results.sarif diff --git a/.github/workflows/scorecard-analysis.yml b/.github/workflows/scorecard-analysis.yml index 7e19968acd..bf9cc6d613 100644 --- a/.github/workflows/scorecard-analysis.yml +++ b/.github/workflows/scorecard-analysis.yml @@ -37,6 +37,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 + uses: github/codeql-action/upload-sarif@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 with: sarif_file: results.sarif diff --git a/Templates/AppSource App/.github/workflows/_BuildALGoProject.yaml b/Templates/AppSource App/.github/workflows/_BuildALGoProject.yaml index 05efacc970..360e39dc39 100644 --- a/Templates/AppSource App/.github/workflows/_BuildALGoProject.yaml +++ b/Templates/AppSource App/.github/workflows/_BuildALGoProject.yaml @@ -148,7 +148,7 @@ jobs: - name: Cache Business Central Artifacts if: steps.DetermineBuildProject.outputs.BuildIt == 'True' && env.useCompilerFolder == 'True' && inputs.useArtifactCache && env.artifactCacheKey - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ${{ runner.temp }}/.artifactcache key: ${{ env.artifactCacheKey }} diff --git a/Templates/Per Tenant Extension/.github/workflows/_BuildALGoProject.yaml b/Templates/Per Tenant Extension/.github/workflows/_BuildALGoProject.yaml index 05efacc970..360e39dc39 100644 --- a/Templates/Per Tenant Extension/.github/workflows/_BuildALGoProject.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/_BuildALGoProject.yaml @@ -148,7 +148,7 @@ jobs: - name: Cache Business Central Artifacts if: steps.DetermineBuildProject.outputs.BuildIt == 'True' && env.useCompilerFolder == 'True' && inputs.useArtifactCache && env.artifactCacheKey - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ${{ runner.temp }}/.artifactcache key: ${{ env.artifactCacheKey }} diff --git a/e2eTests/e2eTestHelper.psm1 b/e2eTests/e2eTestHelper.psm1 index eead106734..557cf0fc0a 100644 --- a/e2eTests/e2eTestHelper.psm1 +++ b/e2eTests/e2eTestHelper.psm1 @@ -1,894 +1,896 @@ -$githubOwner = "githubOwner" -$token = "DefaultToken" -$defaultRepository = "repo" -$defaultApplication = "22.0.0.0" -$defaultRuntime = "10.0" -$defaultPublisher = "MS Test" -$lastTokenRefresh = 0 - -Import-Module (Join-Path $PSScriptRoot "..\Actions\Github-Helper.psm1" -Resolve) -DisableNameChecking -Global -. (Join-Path $PSScriptRoot "..\Actions\AL-Go-Helper.ps1" -Resolve) - -function GetDefaultPublisher() { - return $defaultPublisher -} - -function SetTokenAndRepository { - Param( - [string] $githubOwner, - [string] $token, - [string] $appId, - [string] $appKey, - [string] $repository, - [switch] $github - ) - - $script:githubOwner = $githubOwner - $script:defaultRepository = $repository - - if ($github) { - invoke-git config --global user.email "$githubOwner@users.noreply.github.com" - invoke-git config --global user.name "$githubOwner" - invoke-git config --global hub.protocol https - invoke-git config --global core.autocrlf false - } - - if (-not $github) { - # Running locally - Ensure the user is authenticated with the GitHub CLI. - # This is required for local runs to perform GitHub-related operations. - invoke-gh auth status - gh auth refresh --scopes repo,admin:org,workflow,write:packages,read:packages,delete:packages,user,delete_repo - } elseif ($appKey -and $appId) { - # Running in GitHub Actions - $token = @{ "GitHubAppClientId" = $appId; "PrivateKey" = ($appKey -join '') } | ConvertTo-Json -Compress -Depth 99 - } else { - throw "GitHub App ID and Private Key not set. In order to run end to end tests, you need a Secret called E2E_PRIVATE_KEY and a variable called E2E_APP_ID." - } - - # Repository isn't created yet so authenticating towards the .github repository - RefreshToken -token $token -repository "$githubOwner/.github" -} - -function RefreshToken { - Param( - [Parameter(Mandatory = $false)] - [string] $token, - [Parameter(Mandatory = $true)] - [string] $repository, - [Parameter(Mandatory = $false)] - [switch] $force - ) - if ($github) { - if ($token) { - $script:token = $token - } - - if ($script:token -eq "DefaultToken") { - throw "Token not set." - } - - # Check if the last token refresh was more than 10 minutes ago - - if ((-not $force) -and ($script:lastTokenRefresh -ne 0) -and (([DateTime]::Now - $script:lastTokenRefresh).TotalMinutes -lt 10)) { - return - } - - Write-Host "Authenticating with GitHub using token" - $realToken = GetAccessToken -token $script:token -repository $repository -repositories @() - $script:lastTokenRefresh = [DateTime]::Now - $ENV:GITHUB_TOKEN = $realToken - $ENV:GH_TOKEN = $realToken - invoke-gh auth setup-git # Use GitHub CLI as a credential helper - } else { - $realToken = gh auth token - $ENV:GITHUB_TOKEN = $realToken - $ENV:GH_TOKEN = $realToken - invoke-gh auth setup-git # Use GitHub CLI as a credential helper - } -} - -function Add-PropertiesToJsonFile { - Param( - [string] $path, - [hashTable] $properties, - [switch] $commit, - [switch] $wait - ) - - Write-Host -ForegroundColor Yellow "`nAdd Properties to $([System.IO.Path]::GetFileName($path))" - Write-Host "Properties" - $properties | Out-Host - - $json = Get-Content $path -Encoding UTF8 | ConvertFrom-Json | ConvertTo-HashTable -recurse - $properties.Keys | ForEach-Object { - $json."$_" = $properties."$_" - } - $json | Set-JsonContentLF -path $path - - if ($commit) { - CommitAndPush -commitMessage "Add properties to $([System.IO.Path]::GetFileName($path))" -wait:$wait - } -} - -function Remove-PropertiesFromJsonFile { - Param( - [string] $path, - [string[]] $properties, - [switch] $commit, - [switch] $wait - ) - - Write-Host -ForegroundColor Yellow "`nRemove Properties from $([System.IO.Path]::GetFileName($path))" - Write-Host "Properties" - $properties | Out-Host - - $json = Get-Content $path -Encoding UTF8 | ConvertFrom-Json | ConvertTo-HashTable -recurse - $keys = @($json.Keys) - $keys | ForEach-Object { - $key = $_ - if ($properties | Where-Object { $key -like $_ }) { - $json.Remove($key) - } - } - $json | Set-JsonContentLF -path $path - - if ($commit) { - CommitAndPush -commitMessage "Remove properties from $([System.IO.Path]::GetFileName($path))" -wait:$wait - } -} - -function RunWorkflow { - Param( - [string] $name, - [hashtable] $parameters = @{}, - [switch] $wait, - [string] $repository, - [string] $branch = "main" - ) - - if (!$repository) { - $repository = $defaultRepository - } - Write-Host -ForegroundColor Yellow "`nRun workflow $($name.Trim()) in $repository" - if ($parameters -and $parameters.Count -gt 0) { - Write-Host "Parameters:" - Write-Host ($parameters | ConvertTo-Json) - } - - RefreshToken -repository $repository - - $headers = GetHeaders -token $Env:GH_TOKEN -repository $repository - WaitForRateLimit -headers $headers -displayStatus - - Write-Host "Get Workflows" - $url = "https://api.github.com/repos/$repository/actions/workflows" - $workflows = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflows - $workflows | ForEach-Object { Write-Host "- $($_.Name)"} - if (!$workflows) { - Write-Host "No workflows found, waiting 60 seconds and retrying" - Start-Sleep -seconds 60 - $workflows = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflows - $workflows | ForEach-Object { Write-Host "- $($_.Name)"} - if (!$workflows) { - throw "No workflows found" - } - } - $workflow = $workflows | Where-Object { $_.Name.Trim() -eq $name } - if (!$workflow) { - throw "Workflow $name not found" - } - - Write-Host "Get Previous runs" - $url = "https://api.github.com/repos/$repository/actions/runs" - $previousrun = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.workflow_id -eq $workflow.id -and $_.event -eq 'workflow_dispatch' } | Select-Object -First 1 - if ($previousrun) { - Write-Host "Previous run: $($previousrun.id)" - } - else { - Write-Host "No previous run found" - } - - Write-Host "Run workflow" - $url = "https://api.github.com/repos/$repository/actions/workflows/$($workflow.id)/dispatches" - Write-Host $url - $body = @{ - "ref" = "refs/heads/$branch" - "inputs" = $parameters - } - InvokeWebRequest -Method Post -Headers $headers -Uri $url -Body ($body | ConvertTo-Json) | Out-Null - - Write-Host "Queuing" - do { - Start-Sleep -Seconds 10 - $url = "https://api.github.com/repos/$repository/actions/runs" - $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.workflow_id -eq $workflow.id -and $_.event -eq 'workflow_dispatch' } | Select-Object -First 1 - Write-Host "." - } until (($run) -and ((!$previousrun) -or ($run.id -ne $previousrun.id))) - $runid = $run.id - Write-Host "Run URL: https://github.com/$repository/actions/runs/$runid" - if ($wait) { - WaitWorkflow -repository $repository -runid $run.id - } - $run -} - -function DownloadWorkflowLog { - Param( - [string] $repository, - [string] $runid, - [string] $path - ) - - if (!$repository) { - $repository = $defaultRepository - } - RefreshToken -repository $repository - $headers = GetHeaders -token $ENV:GH_TOKEN -repository $repository - $url = "https://api.github.com/repos/$repository/actions/runs/$runid" - $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json) - $log = InvokeWebRequest -Method Get -Headers $headers -Uri $run.logs_url - $tempFileName = "$([System.IO.Path]::GetTempFileName()).zip" - [System.IO.File]::WriteAllBytes($tempFileName, $log.Content) - Expand-Archive -Path $tempFileName -DestinationPath $path -} - -function CancelAllWorkflows { - Param( - [string] $repository, - [switch] $noDelay - ) - if (-not $noDelay.IsPresent) { - Start-Sleep -Seconds 60 - } - $runs = invoke-gh api /repos/$repository/actions/runs -silent -returnValue | ConvertFrom-Json - foreach($run in $runs.workflow_runs) { - Write-Host $run.name - if ($run.status -eq 'in_progress') { - Write-Host "Cancelling $($run.name) run $($run.id)" - gh api --method POST /repos/$repository/actions/runs/$($run.id)/cancel | Out-Null - } - } -} - -function WaitAllWorkflows { - Param( - [string] $repository, - [switch] $noDelay, - [switch] $noError, - [int] $top = 999 - ) - if (-not $noDelay.IsPresent) { - Start-Sleep -Seconds 60 - } - $runs = invoke-gh api /repos/$repository/actions/runs -silent -returnValue | ConvertFrom-Json - $workflowRuns = $runs.workflow_runs | Select-Object -First $top - foreach($run in $workflowRuns) { - WaitWorkflow -repository $repository -runid $run.id -noDelay -noError:$noError - } -} - -function WaitWorkflow { - Param( - [string] $repository, - [string] $runid, - [switch] $noDelay, - [switch] $noError, - [switch] $noRerun - ) - - $delay = !$noDelay.IsPresent - if (!$repository) { - $repository = $defaultRepository - } - $status = "" - do { - RefreshToken -repository $repository - $headers = GetHeaders -token $ENV:GH_TOKEN -repository $repository - - if ($delay) { - Start-Sleep -Seconds 60 - } - WaitForRateLimit -headers $headers - $url = "https://api.github.com/repos/$repository/actions/runs/$runid" - $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json) - if ($run.status -ne $status) { - $status = $run.status - } - Write-Host "Workflow run is in status $status" - - $delay = $true - } while ($run.status -eq "queued" -or $run.status -eq "in_progress") - - Write-Host "Workflow conclusion: $($run.conclusion)" - - if ($run.conclusion -ne "Success" -and $run.conclusion -ne "cancelled") { - if (-not $noRerun.IsPresent) { - Write-Host "::warning::Rerunning workflow: $($run.name) run $($run.id), conclusion $($run.conclusion), url = $($run.html_url)" - invoke-gh api --method POST /repos/$repository/actions/runs/$runid/rerun | Out-Null - WaitWorkflow -repository $repository -runid $runid -noDelay:$noDelay -noError:$noError -noRerun - } - if (-not $noError.IsPresent) { throw "Workflow $($run.name), conclusion $($run.conclusion), url = $($run.html_url)" } - } -} - -function SetRepositorySecret { - Param( - [string] $repository, - [string] $name, - [string] $value - ) - - if (!$repository) { - $repository = $defaultRepository - } - Write-Host -ForegroundColor Yellow "`nSet Secret $name in $repository" - $value = $value.Replace("`r", '').Replace("`n", '') - gh secret set $name -b $value --repo $repository -} - -<# -.SYNOPSIS - Deletes all workflow runs in a repository. - -.DESCRIPTION - Cleans up workflow runs in a GitHub repository by deleting all existing runs. - This is useful for ensuring a clean state before running tests that track specific workflow runs. - -.PARAMETER repository - The full repository name in the format "owner/repo" (e.g., "microsoft/AL-Go"). - -.EXAMPLE - CleanupWorkflowRuns -repository "microsoft/AL-Go" -#> -function CleanupWorkflowRuns { - Param( - [Parameter(Mandatory = $true)] - [string] $repository - ) - - Write-Host -ForegroundColor Yellow "`nCleaning up workflow runs in $repository" - - RefreshToken -repository $repository - - # Get all workflow runs - $runs = invoke-gh api "/repos/$repository/actions/runs?per_page=100" -silent -returnValue | ConvertFrom-Json - - if ($runs.workflow_runs.Count -eq 0) { - Write-Host "No workflow runs found" - return - } - - Write-Host "Deleting $($runs.workflow_runs.Count) workflow runs..." - foreach ($run in $runs.workflow_runs) { - try { - Write-Host "Deleting run $($run.id) ($($run.name) - $($run.status))" - invoke-gh api /repos/$repository/actions/runs/$($run.id) --method DELETE -silent | Out-Null - } - catch { - Write-Host "Warning: Failed to delete run $($run.id): $_" - } - } - Write-Host "Cleanup completed" -} - -<# -.SYNOPSIS - Resets a repository to match the content of a source repository. - -.DESCRIPTION - Clones the target repository, fetches content from a source repository, and performs a hard reset - followed by a force push. This preserves the repository identity while resetting its content to - match the source repository. Useful for ensuring deterministic state in end-to-end tests. - -.PARAMETER repository - The full name of the target repository to reset in the format "owner/repo" (e.g., "microsoft/AL-Go"). - -.PARAMETER sourceRepository - The full name of the source repository to copy content from in the format "owner/repo". - -.PARAMETER branch - The branch name to reset. Defaults to "main". - -.EXAMPLE - ResetRepositoryToSource -repository "microsoft/test-repo" -sourceRepository "microsoft/source-repo" -branch "main" -#> -function ResetRepositoryToSource { - Param( - [Parameter(Mandatory = $true)] - [string] $repository, - [Parameter(Mandatory = $true)] - [string] $sourceRepository, - [string] $branch = "main" - ) - - Write-Host -ForegroundColor Yellow "`nResetting repository $repository to match $sourceRepository" - - RefreshToken -repository $repository - - # Clone the repository locally if not already in it - $tempPath = [System.IO.Path]::GetTempPath() - $repoPath = Join-Path $tempPath ([System.Guid]::NewGuid().ToString()) - New-Item $repoPath -ItemType Directory | Out-Null - - Push-Location $repoPath - try { - Write-Host "Cloning $repository..." - invoke-gh repo clone $repository . - if ($LASTEXITCODE -ne 0) { - throw "Failed to clone repository $repository" - } - - # Fetch the source repository content - Write-Host "Fetching source repository $sourceRepository..." - invoke-git remote add source "https://github.com/$sourceRepository.git" - if ($LASTEXITCODE -ne 0) { - throw "Failed to add remote source for $sourceRepository" - } - - invoke-git fetch source $branch --quiet - if ($LASTEXITCODE -ne 0) { - throw "Failed to fetch branch $branch from source $sourceRepository" - } - - # Reset the current branch to match the source - Write-Host "Resetting $branch to match source/$branch..." - invoke-git checkout $branch --quiet - if ($LASTEXITCODE -ne 0) { - throw "Failed to checkout branch $branch" - } - - invoke-git reset --hard "source/$branch" --quiet - if ($LASTEXITCODE -ne 0) { - throw "Failed to reset branch $branch to source/$branch" - } - - # Force push to update the repository - Write-Host "Force pushing changes..." - invoke-git push origin $branch --force --quiet - if ($LASTEXITCODE -ne 0) { - throw "Failed to push changes to $repository" - } - - Write-Host "Repository reset completed successfully" - } - catch { - Write-Host "Error resetting repository: $_" - throw - } - finally { - Pop-Location - Remove-Item -Path $repoPath -Force -Recurse -ErrorAction SilentlyContinue - } -} - -function CreateNewAppInFolder { - Param( - [string] $folder, - [string] $id = [GUID]::NewGUID().ToString(), - [int] $objID = 50000, - [string] $name, - [string] $publisher = $defaultPublisher, - [string] $version = "1.0.0.0", - [string] $application = $defaultApplication, - [string] $runtime = $defaultRuntime, - [HashTable[]] $dependencies = @() - ) - - $al = @( - "pageextension $objID ""CustListExt$name"" extends ""Customer List""" - "{" - " trigger OnOpenPage();" - " begin" - " Message('App published: Hello $name!');" - " end;" - "}") - $appJson = [ordered]@{ - "id" = $id - "name" = $name - "version" = $version - "publisher" = $publisher - "dependencies" = $dependencies - "application" = $application - "runtime" = $runtime - "idRanges" = @( @{ "from" = $objID; "to" = $objID } ) - "resourceExposurePolicy" = @{ "allowDebugging" = $true; "allowDownloadingSource" = $true; "includeSourceInSymbolFile" = $true } - } - $folder = Join-Path $folder $name - New-Item -Path $folder -ItemType Directory | Out-Null - $appJson | Set-JsonContentLF -Path (Join-Path $folder "app.json") - $al -join "`n" | Set-ContentLF -Path (Join-Path $folder "$name.al") - $id -} - -function ModifyAppInFolder { - Param( - [string] $folder, - [string] $name, - [string] $message = "Modify $name", - [switch] $commit, - [switch] $wait - ) - $alFile = Join-Path $folder "$name.al" - $al = Get-Content -Encoding utf8 -Path $alFile - # Add another ! to the end of the message and save - ($al -join "`n").Replace("!');","!!');") | Set-ContentLF -Path (Join-Path $folder "$name.al") - if ($commit) { - CommitAndPush -commitMessage $message -wait:$wait - } -} - -function CreateAlGoRepository { - Param( - [switch] $github, - [string] $repository, - [string] $template = "", - [string[]] $projects = @(), - [string] $contentPath, - [scriptBlock] $contentScript, - [switch] $private, - [switch] $linux, - [string] $branch = "main", - [hashtable] $addRepoSettings = @{} - ) - - if (!$repository) { - $repository = $defaultRepository - } - $waitMinutes = 0 - if ($github) { - $waitMinutes = Get-Random -Minimum 0 -Maximum 4 - } - $templateFolder = '' - if ($template.Contains('|')) { - # In order to run tests on the direct AL-Go Development branch, specify the folder in which the template is located after a | character in template - # example: "https://github.com/freddydk/AL-Go@branch|Templates/Per Tenant Extension" - $templateFolder = $template.Split('|')[1] - $templateOwner = $template.Split('/')[3] - $template = $template.Split('|')[0] - $waitMinutes = 0 # Do not wait when running tests on direct AL-Go Development branch - } - if (!$template.Contains('@')) { - $template += '@main' - } - $templateBranch = $template.Split('@')[1] - $templateRepo = $template.Split('@')[0] - - $tempPath = [System.IO.Path]::GetTempPath() - $path = Join-Path $tempPath ( [System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetTempFileName())) - New-Item $path -ItemType Directory | Out-Null - Set-Location $path - if ($waitMinutes) { - Write-Host "Waiting $waitMinutes minutes" - Start-Sleep -seconds ($waitMinutes*60) - } - if ($private) { - Write-Host -ForegroundColor Yellow "`nCreating private repository $repository (based on $template)" - invoke-gh repo create $repository --private --clone - } - else { - Write-Host -ForegroundColor Yellow "`nCreating public repository $repository (based on $template)" - invoke-gh repo create $repository --public --clone - } - Start-Sleep -seconds 10 - Set-Location '*' - - $templateUrl = "$templateRepo/archive/refs/heads/$templateBranch.zip" - Write-Host "Downloading template from $templateUrl" - $zipFileName = Join-Path $tempPath "$([GUID]::NewGuid().ToString()).zip" - [System.Net.WebClient]::new().DownloadFile($templateUrl, $zipFileName) - - $tempRepoPath = Join-Path $tempPath ([GUID]::NewGuid().ToString()) - Expand-Archive -Path $zipFileName -DestinationPath $tempRepoPath - Copy-Item (Join-Path (Get-Item "$tempRepoPath/*/$templateFolder").FullName '*') -Destination . -Recurse -Force - Remove-Item -Path $tempRepoPath -Force -Recurse - Remove-Item -Path $zipFileName -Force - if ($templateFolder) { - # This is a direct AL-Go development repository - # Replace URL's + references to microsoft/AL-Go-Actions with $templateOwner/AL-Go/Actions - Get-ChildItem -Path . -File -Recurse | ForEach-Object { - $file = $_.FullName - $lines = Get-Content -Encoding UTF8 -path $file - - # Replace URL's to actions repository first - $regex = "^(.*)https:\/\/raw\.githubusercontent\.com\/microsoft\/AL-Go-Actions\/main(.*)$" - $replace = "`${1}https://raw.githubusercontent.com/$($templateOwner)/AL-Go/$($templateBranch)/Actions`${2}" - $lines = $lines | ForEach-Object { $_ -replace $regex, $replace } - - # Replace AL-Go-Actions references - $regex = "^(.*)microsoft\/AL-Go-Actions(.*)main(.*)$" - $replace = "`${1}$($templateOwner)/AL-Go/Actions`${2}$($templateBranch)`${3}" - $lines = $lines | ForEach-Object { $_ -replace $regex, $replace } - - $content = "$($lines -join "`n")`n" - - # Update Template references in test apps - $content = $content.Replace('{TEMPLATEURL}', $template) - $content = $content.Replace('https://github.com/microsoft/AL-Go-PTE@main', $template) - $content = $content.Replace('https://github.com/microsoft/AL-Go-AppSource@main', $template) - - [System.IO.File]::WriteAllText($file, $content) - } - } - - if ($projects) { - # Make Repo multi-project - $projects | ForEach-Object { - New-Item $_ -ItemType Directory | Out-Null - Copy-Item '.AL-Go' -Destination $_ -Recurse -Force - } - Remove-Item '.AL-Go' -Force -Recurse - } - if ($contentPath) { - Write-Host "Copy content from $contentPath" - Copy-Item (Join-Path $contentPath "*") -Destination . -Recurse -Force - } - if ($contentScript) { - & $contentScript -path (get-location).Path - } - $repoSettingsFile = ".github\AL-Go-Settings.json" - $repoSettings = Get-Content $repoSettingsFile -Encoding UTF8 | ConvertFrom-Json - $runson = "windows-latest" - $shell = "powershell" - if ($linux) { - $runson = "ubuntu-latest" - $shell = "pwsh" - } - - if ($runson -ne "windows-latest" -or $shell -ne "powershell") { - $repoSettings | Add-Member -MemberType NoteProperty -Name "runs-on" -Value $runson - $repoSettings | Add-Member -MemberType NoteProperty -Name "shell" -Value $shell - Get-ChildItem -Path '.\.github\workflows\*.yaml' | ForEach-Object { - Write-Host $_.FullName - $content = Get-ContentLF -Path $_.FullName - $srcPattern = "runs-on: [ windows-latest ]`n" - $replacePattern = "runs-on: [ $runson ]`n" - $content = "$content`n".Replace($srcPattern, $replacePattern).TrimEnd("`n") - $srcPattern = "shell: powershell`n" - $replacePattern = "shell: $shell`n" - $content = "$content`n".Replace($srcPattern, $replacePattern).TrimEnd("`n") - [System.IO.File]::WriteAllText($_.FullName, $content) - } - } - # Disable telemetry AL-Go and BcContainerHelper telemetry when running end-2-end tests - $repoSettings | Add-Member -MemberType NoteProperty -Name "MicrosoftTelemetryConnectionString" -Value "" - $repoSettings | Set-JsonContentLF -path $repoSettingsFile - if ($addRepoSettings.Keys.Count) { - Add-PropertiesToJsonFile -path $repoSettingsFile -properties $addRepoSettings - } - - RefreshToken -repository $repository - - invoke-git add * - invoke-git commit --allow-empty -m 'init' - invoke-git branch -M $branch - if ($githubOwner) { - if ($github) { - invoke-git remote set-url origin "https://$($githubOwner)@github.com/$repository.git" - } else { - invoke-git remote set-url origin "https://github.com/$repository" - } - } - invoke-git push --set-upstream origin $branch - if (!$github) { - Start-Process "https://github.com/$repository/actions" - } - Start-Sleep -seconds 10 -} - -function Pull { - Param( - [string] $branch = "main" - ) - - invoke-git pull origin $branch -} - -function CommitAndPush { - Param( - [string] $commitMessage = "commitmessage", - [switch] $wait - ) - - if (!$repository) { - $repository = $defaultRepository - } - - RefreshToken -repository $repository - - if ($wait) { - $headers = GetHeaders -token $ENV:GH_TOKEN - Write-Host "Get Previous runs" - $url = "https://api.github.com/repos/$repository/actions/runs" - $previousrunids = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Select-Object -ExpandProperty id - if ($previousrunids) { - Write-Host "Previous runs: $($previousrunids -join ', ')" - } - else { - Write-Host "No previous runs found" - } - } - invoke-git add * - invoke-git commit --allow-empty -m "'$commitMessage'" - invoke-git push - if ($wait) { - while ($true) { - Start-Sleep -Seconds 10 - $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Where-Object { $previousrunids -notcontains $_.id } - if ($run) { - break - } - Write-Host "Run not started, waiting..." - } - WaitWorkflow -repository $repository -runid $run.id - $run - } -} - -function MergePRandPull { - Param( - [string] $repository, - [string] $branch = "main", - [switch] $wait - ) - - if (!$repository) { - $repository = $defaultRepository - } - - Write-Host "Get Previous runs" - RefreshToken -repository $repository - $headers = GetHeaders -token $ENV:GH_TOKEN -repository $repository - $url = "https://api.github.com/repos/$repository/actions/runs" - $previousrunids = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Select-Object -ExpandProperty id - if ($previousrunids) { - Write-Host "Previous runs: $($previousrunids -join ', ')" - } - else { - Write-Host "No previous runs found" - } - - $phs = @(invoke-gh -returnValue pr list --repo $repository) - if ($phs.Count -eq 0) { - throw "No Pull Request was created" - } - elseif ($phs.Count -gt 1) { - throw "More than one Pull Request exists" - } - $prid = $phs.Split("`t")[0] - Write-Host -ForegroundColor Yellow "`nMerge Pull Request $prid into repository $repository" - invoke-gh pr merge $prid --squash --delete-branch --repo $repository | Out-Host - while ($true) { - Start-Sleep -Seconds 10 - $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Where-Object { $previousrunids -notcontains $_.id } - if ($run) { - break - } - Write-Host "Run not started, waiting..." - } - if ($wait) { - WaitWorkflow -repository $repository -runid $run.id - } - Write-Host "Merge commit run: $($run.id)" - $run - Pull -branch $branch -} - -function RemoveRepository { - Param( - [string] $repository, - [string] $path = "" - ) - - if (!$repository) { - $repository = $defaultRepository - } - if ($repository) { - try { - $owner = $repository.Split("/")[0] - Write-Host -ForegroundColor Yellow "`nRemoving repository $repository" - # Remove all packages belonging to the repository - $ownerType = invoke-gh api users/$owner --jq .type -silent -returnValue - if ($ownerType -eq 'User') { - # Package belongs to a user - $ownerStr = "users/$owner" - } - else { - # Package belongs to an organization - $ownerStr = "orgs/$owner" - } - @((invoke-gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /$ownerStr/packages?package_type=nuget -silent -returnvalue -ErrorAction SilentlyContinue | ConvertFrom-Json)) | Where-Object { $_.PSObject.Properties.Name -eq 'repository' } | Where-Object { $_.repository.full_name -eq $repository } | ForEach-Object { - Write-Host "+ package $($_.name)" - # Pipe empty string into GH API --METHOD DELETE due to https://github.com/cli/cli/issues/3937 - '' | invoke-gh api --method DELETE -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /$ownerStr/packages/nuget/$($_.name) --input - } - } - catch { - Write-Host -ForegroundColor Red "Error removing packages" - Write-Host -ForegroundColor Red $_.Exception.Message - } - invoke-gh repo delete $repository --yes | Out-Host - } - - if ($path) { - if (-not $path.StartsWith("$([System.IO.Path]::GetTempPath())",[StringComparison]::InvariantCultureIgnoreCase)) { - throw "$path is not temppath" - } - else { - Set-Location ([System.IO.Path]::GetTempPath()) - Remove-Item $path -Recurse -Force - } - } -} - -function Test-LogContainsFromRun { - Param( - [string] $repository, - [string] $runid, - [string] $jobName, - [string] $stepName, - [string] $expectedText, - [switch] $isRegEx - ) - - DownloadWorkflowLog -repository $repository -runid $runid -path 'logs' - try { - # Log format changes are rolling out on GitHub Actions, we have to support both - $oldStepLogFile = "logs/$jobName/*_$stepName.txt" - $newJobLogFile = "logs/*_$jobName.txt" - if (Test-Path -Path $oldStepLogFile) { - $runPipelineLog = Get-Content -Path (Get-Item $oldStepLogFile).FullName -encoding utf8 -raw - } - else { - $jobLog = Get-Content -Path (Get-Item $newJobLogFile).FullName -encoding utf8 - $emit = $false - $runPipelineLog = @($jobLog | ForEach-Object { - if ($emit -and $_ -like "*##[[]group]Run *@*") { - Write-Host -ForegroundColor Yellow "Foundend $_" - $emit = $false - } - elseif ($_ -like "*##[[]group]Run *$StepName@*") { - Write-Host -ForegroundColor Yellow "Foundstart $_" - $emit = $true - } - else { - Write-Host -ForegroundColor Gray $_ - } - if ($emit) { $_ } - }) -join "`n" - } - - if ($isRegEx) { - $found = $runPipelineLog -match $expectedText - return $Matches - } - else { - $found = $runPipelineLog.indexOf($expectedText, [System.StringComparison]::OrdinalIgnoreCase) -ne -1 - } - - if ($found) { - Write-Host "'$expectedText' found in the log for '$jobName -> $stepName' as expected" - } - else { - throw "Expected to find '$expectedText' in the log for '$jobName -> $stepName', but did not find it" - } - } - finally { - Remove-Item -Path 'logs' -Recurse -Force - } -} - -. (Join-Path $PSScriptRoot "Workflows\RunAddExistingAppOrTestApp.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunCICD.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunCreateApp.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunDeployReferenceDocumentation.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunCreateOnlineDevelopmentEnvironment.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunCreateRelease.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunCreateTestApp.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunIncrementVersionNumber.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunPublishToEnvironment.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunUpdateAlGoSystemFiles.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunTestCurrent.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunTestNextMinor.ps1") -. (Join-Path $PSScriptRoot "Workflows\RunTestNextMajor.ps1") - -. (Join-Path $PSScriptRoot "Test-Functions.ps1") +$githubOwner = "githubOwner" +$token = "DefaultToken" +$defaultRepository = "repo" +$defaultApplication = "22.0.0.0" +$defaultRuntime = "10.0" +$defaultPublisher = "MS Test" +$lastTokenRefresh = 0 + +Import-Module (Join-Path $PSScriptRoot "..\Actions\Github-Helper.psm1" -Resolve) -DisableNameChecking -Global +. (Join-Path $PSScriptRoot "..\Actions\AL-Go-Helper.ps1" -Resolve) + +function GetDefaultPublisher() { + return $defaultPublisher +} + +function SetTokenAndRepository { + Param( + [string] $githubOwner, + [string] $token, + [string] $appId, + [string] $appKey, + [string] $repository, + [switch] $github + ) + + $script:githubOwner = $githubOwner + $script:defaultRepository = $repository + + if ($github) { + invoke-git config --global user.email "$githubOwner@users.noreply.github.com" + invoke-git config --global user.name "$githubOwner" + invoke-git config --global hub.protocol https + invoke-git config --global core.autocrlf false + } + + if (-not $github) { + # Running locally - Ensure the user is authenticated with the GitHub CLI. + # This is required for local runs to perform GitHub-related operations. + invoke-gh auth status + gh auth refresh --scopes repo,admin:org,workflow,write:packages,read:packages,delete:packages,user,delete_repo + } elseif ($appKey -and $appId) { + # Running in GitHub Actions + $token = @{ "GitHubAppClientId" = $appId; "PrivateKey" = ($appKey -join '') } | ConvertTo-Json -Compress -Depth 99 + } else { + throw "GitHub App ID and Private Key not set. In order to run end to end tests, you need a Secret called E2E_PRIVATE_KEY and a variable called E2E_APP_ID." + } + + # Repository isn't created yet so authenticating towards the .github repository + RefreshToken -token $token -repository "$githubOwner/.github" +} + +function RefreshToken { + Param( + [Parameter(Mandatory = $false)] + [string] $token, + [Parameter(Mandatory = $true)] + [string] $repository, + [Parameter(Mandatory = $false)] + [switch] $force + ) + if ($github) { + if ($token) { + $script:token = $token + } + + if ($script:token -eq "DefaultToken") { + throw "Token not set." + } + + # Check if the last token refresh was more than 10 minutes ago + + if ((-not $force) -and ($script:lastTokenRefresh -ne 0) -and (([DateTime]::Now - $script:lastTokenRefresh).TotalMinutes -lt 10)) { + return + } + + Write-Host "Authenticating with GitHub using token" + $realToken = GetAccessToken -token $script:token -repository $repository -repositories @() + $script:lastTokenRefresh = [DateTime]::Now + $ENV:GITHUB_TOKEN = $realToken + $ENV:GH_TOKEN = $realToken + invoke-gh auth setup-git # Use GitHub CLI as a credential helper + } else { + $realToken = gh auth token + $ENV:GITHUB_TOKEN = $realToken + $ENV:GH_TOKEN = $realToken + invoke-gh auth setup-git # Use GitHub CLI as a credential helper + } +} + +function Add-PropertiesToJsonFile { + Param( + [string] $path, + [hashTable] $properties, + [switch] $commit, + [switch] $wait + ) + + Write-Host -ForegroundColor Yellow "`nAdd Properties to $([System.IO.Path]::GetFileName($path))" + Write-Host "Properties" + $properties | Out-Host + + $json = Get-Content $path -Encoding UTF8 | ConvertFrom-Json | ConvertTo-HashTable -recurse + $properties.Keys | ForEach-Object { + $json."$_" = $properties."$_" + } + $json | Set-JsonContentLF -path $path + + if ($commit) { + CommitAndPush -commitMessage "Add properties to $([System.IO.Path]::GetFileName($path))" -wait:$wait + } +} + +function Remove-PropertiesFromJsonFile { + Param( + [string] $path, + [string[]] $properties, + [switch] $commit, + [switch] $wait + ) + + Write-Host -ForegroundColor Yellow "`nRemove Properties from $([System.IO.Path]::GetFileName($path))" + Write-Host "Properties" + $properties | Out-Host + + $json = Get-Content $path -Encoding UTF8 | ConvertFrom-Json | ConvertTo-HashTable -recurse + $keys = @($json.Keys) + $keys | ForEach-Object { + $key = $_ + if ($properties | Where-Object { $key -like $_ }) { + $json.Remove($key) + } + } + $json | Set-JsonContentLF -path $path + + if ($commit) { + CommitAndPush -commitMessage "Remove properties from $([System.IO.Path]::GetFileName($path))" -wait:$wait + } +} + +function RunWorkflow { + Param( + [string] $name, + [hashtable] $parameters = @{}, + [switch] $wait, + [string] $repository, + [string] $branch = "main" + ) + + if (!$repository) { + $repository = $defaultRepository + } + Write-Host -ForegroundColor Yellow "`nRun workflow $($name.Trim()) in $repository" + if ($parameters -and $parameters.Count -gt 0) { + Write-Host "Parameters:" + Write-Host ($parameters | ConvertTo-Json) + } + + RefreshToken -repository $repository + + $headers = GetHeaders -token $Env:GH_TOKEN -repository $repository + WaitForRateLimit -headers $headers -displayStatus + + Write-Host "Get Workflows" + $url = "https://api.github.com/repos/$repository/actions/workflows" + $workflows = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflows + $workflows | ForEach-Object { Write-Host "- $($_.Name)"} + if (!$workflows) { + Write-Host "No workflows found, waiting 60 seconds and retrying" + Start-Sleep -seconds 60 + $workflows = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflows + $workflows | ForEach-Object { Write-Host "- $($_.Name)"} + if (!$workflows) { + throw "No workflows found" + } + } + $workflow = $workflows | Where-Object { $_.Name.Trim() -eq $name } + if (!$workflow) { + throw "Workflow $name not found" + } + + Write-Host "Get Previous runs" + $url = "https://api.github.com/repos/$repository/actions/runs" + $previousrun = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.workflow_id -eq $workflow.id -and $_.event -eq 'workflow_dispatch' } | Select-Object -First 1 + if ($previousrun) { + Write-Host "Previous run: $($previousrun.id)" + } + else { + Write-Host "No previous run found" + } + + Write-Host "Run workflow" + $url = "https://api.github.com/repos/$repository/actions/workflows/$($workflow.id)/dispatches" + Write-Host $url + $body = @{ + "ref" = "refs/heads/$branch" + "inputs" = $parameters + } + InvokeWebRequest -Method Post -Headers $headers -Uri $url -Body ($body | ConvertTo-Json) | Out-Null + + Write-Host "Queuing" + do { + Start-Sleep -Seconds 10 + $url = "https://api.github.com/repos/$repository/actions/runs" + $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.workflow_id -eq $workflow.id -and $_.event -eq 'workflow_dispatch' } | Select-Object -First 1 + Write-Host "." + } until (($run) -and ((!$previousrun) -or ($run.id -ne $previousrun.id))) + $runid = $run.id + Write-Host "Run URL: https://github.com/$repository/actions/runs/$runid" + if ($wait) { + WaitWorkflow -repository $repository -runid $run.id + } + $run +} + +function DownloadWorkflowLog { + Param( + [string] $repository, + [string] $runid, + [string] $path + ) + + if (!$repository) { + $repository = $defaultRepository + } + RefreshToken -repository $repository + $headers = GetHeaders -token $ENV:GH_TOKEN -repository $repository + $url = "https://api.github.com/repos/$repository/actions/runs/$runid" + $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json) + $log = InvokeWebRequest -Method Get -Headers $headers -Uri $run.logs_url + $tempFileName = "$([System.IO.Path]::GetTempFileName()).zip" + [System.IO.File]::WriteAllBytes($tempFileName, $log.Content) + Expand-Archive -Path $tempFileName -DestinationPath $path +} + +function CancelAllWorkflows { + Param( + [string] $repository, + [switch] $noDelay + ) + if (-not $noDelay.IsPresent) { + Start-Sleep -Seconds 60 + } + $runs = invoke-gh api /repos/$repository/actions/runs -silent -returnValue | ConvertFrom-Json + foreach($run in $runs.workflow_runs) { + Write-Host $run.name + if ($run.status -eq 'in_progress') { + Write-Host "Cancelling $($run.name) run $($run.id)" + gh api --method POST /repos/$repository/actions/runs/$($run.id)/cancel | Out-Null + } + } +} + +function WaitAllWorkflows { + Param( + [string] $repository, + [switch] $noDelay, + [switch] $noError, + [int] $top = 999 + ) + if (-not $noDelay.IsPresent) { + Start-Sleep -Seconds 60 + } + $runs = invoke-gh api /repos/$repository/actions/runs -silent -returnValue | ConvertFrom-Json + $workflowRuns = $runs.workflow_runs | Select-Object -First $top + foreach($run in $workflowRuns) { + WaitWorkflow -repository $repository -runid $run.id -noDelay -noError:$noError + } +} + +function WaitWorkflow { + Param( + [string] $repository, + [string] $runid, + [switch] $noDelay, + [switch] $noError, + [switch] $noRerun + ) + + $delay = !$noDelay.IsPresent + if (!$repository) { + $repository = $defaultRepository + } + $status = "" + do { + RefreshToken -repository $repository + $headers = GetHeaders -token $ENV:GH_TOKEN -repository $repository + + if ($delay) { + Start-Sleep -Seconds 60 + } + WaitForRateLimit -headers $headers + $url = "https://api.github.com/repos/$repository/actions/runs/$runid" + $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json) + if ($run.status -ne $status) { + $status = $run.status + } + Write-Host "Workflow run is in status $status" + + $delay = $true + } while ($run.status -eq "queued" -or $run.status -eq "in_progress") + + Write-Host "Workflow conclusion: $($run.conclusion)" + + if ($run.conclusion -ne "Success" -and $run.conclusion -ne "cancelled") { + if (-not $noRerun.IsPresent) { + Write-Host "::warning::Rerunning workflow: $($run.name) run $($run.id), conclusion $($run.conclusion), url = $($run.html_url)" + invoke-gh api --method POST /repos/$repository/actions/runs/$runid/rerun | Out-Null + WaitWorkflow -repository $repository -runid $runid -noDelay:$noDelay -noError:$noError -noRerun + } + else { + if (-not $noError.IsPresent) { throw "Workflow $($run.name), conclusion $($run.conclusion), url = $($run.html_url)" } + } + } +} + +function SetRepositorySecret { + Param( + [string] $repository, + [string] $name, + [string] $value + ) + + if (!$repository) { + $repository = $defaultRepository + } + Write-Host -ForegroundColor Yellow "`nSet Secret $name in $repository" + $value = $value.Replace("`r", '').Replace("`n", '') + gh secret set $name -b $value --repo $repository +} + +<# +.SYNOPSIS + Deletes all workflow runs in a repository. + +.DESCRIPTION + Cleans up workflow runs in a GitHub repository by deleting all existing runs. + This is useful for ensuring a clean state before running tests that track specific workflow runs. + +.PARAMETER repository + The full repository name in the format "owner/repo" (e.g., "microsoft/AL-Go"). + +.EXAMPLE + CleanupWorkflowRuns -repository "microsoft/AL-Go" +#> +function CleanupWorkflowRuns { + Param( + [Parameter(Mandatory = $true)] + [string] $repository + ) + + Write-Host -ForegroundColor Yellow "`nCleaning up workflow runs in $repository" + + RefreshToken -repository $repository + + # Get all workflow runs + $runs = invoke-gh api "/repos/$repository/actions/runs?per_page=100" -silent -returnValue | ConvertFrom-Json + + if ($runs.workflow_runs.Count -eq 0) { + Write-Host "No workflow runs found" + return + } + + Write-Host "Deleting $($runs.workflow_runs.Count) workflow runs..." + foreach ($run in $runs.workflow_runs) { + try { + Write-Host "Deleting run $($run.id) ($($run.name) - $($run.status))" + invoke-gh api /repos/$repository/actions/runs/$($run.id) --method DELETE -silent | Out-Null + } + catch { + Write-Host "Warning: Failed to delete run $($run.id): $_" + } + } + Write-Host "Cleanup completed" +} + +<# +.SYNOPSIS + Resets a repository to match the content of a source repository. + +.DESCRIPTION + Clones the target repository, fetches content from a source repository, and performs a hard reset + followed by a force push. This preserves the repository identity while resetting its content to + match the source repository. Useful for ensuring deterministic state in end-to-end tests. + +.PARAMETER repository + The full name of the target repository to reset in the format "owner/repo" (e.g., "microsoft/AL-Go"). + +.PARAMETER sourceRepository + The full name of the source repository to copy content from in the format "owner/repo". + +.PARAMETER branch + The branch name to reset. Defaults to "main". + +.EXAMPLE + ResetRepositoryToSource -repository "microsoft/test-repo" -sourceRepository "microsoft/source-repo" -branch "main" +#> +function ResetRepositoryToSource { + Param( + [Parameter(Mandatory = $true)] + [string] $repository, + [Parameter(Mandatory = $true)] + [string] $sourceRepository, + [string] $branch = "main" + ) + + Write-Host -ForegroundColor Yellow "`nResetting repository $repository to match $sourceRepository" + + RefreshToken -repository $repository + + # Clone the repository locally if not already in it + $tempPath = [System.IO.Path]::GetTempPath() + $repoPath = Join-Path $tempPath ([System.Guid]::NewGuid().ToString()) + New-Item $repoPath -ItemType Directory | Out-Null + + Push-Location $repoPath + try { + Write-Host "Cloning $repository..." + invoke-gh repo clone $repository . + if ($LASTEXITCODE -ne 0) { + throw "Failed to clone repository $repository" + } + + # Fetch the source repository content + Write-Host "Fetching source repository $sourceRepository..." + invoke-git remote add source "https://github.com/$sourceRepository.git" + if ($LASTEXITCODE -ne 0) { + throw "Failed to add remote source for $sourceRepository" + } + + invoke-git fetch source $branch --quiet + if ($LASTEXITCODE -ne 0) { + throw "Failed to fetch branch $branch from source $sourceRepository" + } + + # Reset the current branch to match the source + Write-Host "Resetting $branch to match source/$branch..." + invoke-git checkout $branch --quiet + if ($LASTEXITCODE -ne 0) { + throw "Failed to checkout branch $branch" + } + + invoke-git reset --hard "source/$branch" --quiet + if ($LASTEXITCODE -ne 0) { + throw "Failed to reset branch $branch to source/$branch" + } + + # Force push to update the repository + Write-Host "Force pushing changes..." + invoke-git push origin $branch --force --quiet + if ($LASTEXITCODE -ne 0) { + throw "Failed to push changes to $repository" + } + + Write-Host "Repository reset completed successfully" + } + catch { + Write-Host "Error resetting repository: $_" + throw + } + finally { + Pop-Location + Remove-Item -Path $repoPath -Force -Recurse -ErrorAction SilentlyContinue + } +} + +function CreateNewAppInFolder { + Param( + [string] $folder, + [string] $id = [GUID]::NewGUID().ToString(), + [int] $objID = 50000, + [string] $name, + [string] $publisher = $defaultPublisher, + [string] $version = "1.0.0.0", + [string] $application = $defaultApplication, + [string] $runtime = $defaultRuntime, + [HashTable[]] $dependencies = @() + ) + + $al = @( + "pageextension $objID ""CustListExt$name"" extends ""Customer List""" + "{" + " trigger OnOpenPage();" + " begin" + " Message('App published: Hello $name!');" + " end;" + "}") + $appJson = [ordered]@{ + "id" = $id + "name" = $name + "version" = $version + "publisher" = $publisher + "dependencies" = $dependencies + "application" = $application + "runtime" = $runtime + "idRanges" = @( @{ "from" = $objID; "to" = $objID } ) + "resourceExposurePolicy" = @{ "allowDebugging" = $true; "allowDownloadingSource" = $true; "includeSourceInSymbolFile" = $true } + } + $folder = Join-Path $folder $name + New-Item -Path $folder -ItemType Directory | Out-Null + $appJson | Set-JsonContentLF -Path (Join-Path $folder "app.json") + $al -join "`n" | Set-ContentLF -Path (Join-Path $folder "$name.al") + $id +} + +function ModifyAppInFolder { + Param( + [string] $folder, + [string] $name, + [string] $message = "Modify $name", + [switch] $commit, + [switch] $wait + ) + $alFile = Join-Path $folder "$name.al" + $al = Get-Content -Encoding utf8 -Path $alFile + # Add another ! to the end of the message and save + ($al -join "`n").Replace("!');","!!');") | Set-ContentLF -Path (Join-Path $folder "$name.al") + if ($commit) { + CommitAndPush -commitMessage $message -wait:$wait + } +} + +function CreateAlGoRepository { + Param( + [switch] $github, + [string] $repository, + [string] $template = "", + [string[]] $projects = @(), + [string] $contentPath, + [scriptBlock] $contentScript, + [switch] $private, + [switch] $linux, + [string] $branch = "main", + [hashtable] $addRepoSettings = @{} + ) + + if (!$repository) { + $repository = $defaultRepository + } + $waitMinutes = 0 + if ($github) { + $waitMinutes = Get-Random -Minimum 0 -Maximum 4 + } + $templateFolder = '' + if ($template.Contains('|')) { + # In order to run tests on the direct AL-Go Development branch, specify the folder in which the template is located after a | character in template + # example: "https://github.com/freddydk/AL-Go@branch|Templates/Per Tenant Extension" + $templateFolder = $template.Split('|')[1] + $templateOwner = $template.Split('/')[3] + $template = $template.Split('|')[0] + $waitMinutes = 0 # Do not wait when running tests on direct AL-Go Development branch + } + if (!$template.Contains('@')) { + $template += '@main' + } + $templateBranch = $template.Split('@')[1] + $templateRepo = $template.Split('@')[0] + + $tempPath = [System.IO.Path]::GetTempPath() + $path = Join-Path $tempPath ( [System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetTempFileName())) + New-Item $path -ItemType Directory | Out-Null + Set-Location $path + if ($waitMinutes) { + Write-Host "Waiting $waitMinutes minutes" + Start-Sleep -seconds ($waitMinutes*60) + } + if ($private) { + Write-Host -ForegroundColor Yellow "`nCreating private repository $repository (based on $template)" + invoke-gh repo create $repository --private --clone + } + else { + Write-Host -ForegroundColor Yellow "`nCreating public repository $repository (based on $template)" + invoke-gh repo create $repository --public --clone + } + Start-Sleep -seconds 10 + Set-Location '*' + + $templateUrl = "$templateRepo/archive/refs/heads/$templateBranch.zip" + Write-Host "Downloading template from $templateUrl" + $zipFileName = Join-Path $tempPath "$([GUID]::NewGuid().ToString()).zip" + [System.Net.WebClient]::new().DownloadFile($templateUrl, $zipFileName) + + $tempRepoPath = Join-Path $tempPath ([GUID]::NewGuid().ToString()) + Expand-Archive -Path $zipFileName -DestinationPath $tempRepoPath + Copy-Item (Join-Path (Get-Item "$tempRepoPath/*/$templateFolder").FullName '*') -Destination . -Recurse -Force + Remove-Item -Path $tempRepoPath -Force -Recurse + Remove-Item -Path $zipFileName -Force + if ($templateFolder) { + # This is a direct AL-Go development repository + # Replace URL's + references to microsoft/AL-Go-Actions with $templateOwner/AL-Go/Actions + Get-ChildItem -Path . -File -Recurse | ForEach-Object { + $file = $_.FullName + $lines = Get-Content -Encoding UTF8 -path $file + + # Replace URL's to actions repository first + $regex = "^(.*)https:\/\/raw\.githubusercontent\.com\/microsoft\/AL-Go-Actions\/main(.*)$" + $replace = "`${1}https://raw.githubusercontent.com/$($templateOwner)/AL-Go/$($templateBranch)/Actions`${2}" + $lines = $lines | ForEach-Object { $_ -replace $regex, $replace } + + # Replace AL-Go-Actions references + $regex = "^(.*)microsoft\/AL-Go-Actions(.*)main(.*)$" + $replace = "`${1}$($templateOwner)/AL-Go/Actions`${2}$($templateBranch)`${3}" + $lines = $lines | ForEach-Object { $_ -replace $regex, $replace } + + $content = "$($lines -join "`n")`n" + + # Update Template references in test apps + $content = $content.Replace('{TEMPLATEURL}', $template) + $content = $content.Replace('https://github.com/microsoft/AL-Go-PTE@main', $template) + $content = $content.Replace('https://github.com/microsoft/AL-Go-AppSource@main', $template) + + [System.IO.File]::WriteAllText($file, $content) + } + } + + if ($projects) { + # Make Repo multi-project + $projects | ForEach-Object { + New-Item $_ -ItemType Directory | Out-Null + Copy-Item '.AL-Go' -Destination $_ -Recurse -Force + } + Remove-Item '.AL-Go' -Force -Recurse + } + if ($contentPath) { + Write-Host "Copy content from $contentPath" + Copy-Item (Join-Path $contentPath "*") -Destination . -Recurse -Force + } + if ($contentScript) { + & $contentScript -path (get-location).Path + } + $repoSettingsFile = ".github\AL-Go-Settings.json" + $repoSettings = Get-Content $repoSettingsFile -Encoding UTF8 | ConvertFrom-Json + $runson = "windows-latest" + $shell = "powershell" + if ($linux) { + $runson = "ubuntu-latest" + $shell = "pwsh" + } + + if ($runson -ne "windows-latest" -or $shell -ne "powershell") { + $repoSettings | Add-Member -MemberType NoteProperty -Name "runs-on" -Value $runson + $repoSettings | Add-Member -MemberType NoteProperty -Name "shell" -Value $shell + Get-ChildItem -Path '.\.github\workflows\*.yaml' | ForEach-Object { + Write-Host $_.FullName + $content = Get-ContentLF -Path $_.FullName + $srcPattern = "runs-on: [ windows-latest ]`n" + $replacePattern = "runs-on: [ $runson ]`n" + $content = "$content`n".Replace($srcPattern, $replacePattern).TrimEnd("`n") + $srcPattern = "shell: powershell`n" + $replacePattern = "shell: $shell`n" + $content = "$content`n".Replace($srcPattern, $replacePattern).TrimEnd("`n") + [System.IO.File]::WriteAllText($_.FullName, $content) + } + } + # Disable telemetry AL-Go and BcContainerHelper telemetry when running end-2-end tests + $repoSettings | Add-Member -MemberType NoteProperty -Name "MicrosoftTelemetryConnectionString" -Value "" + $repoSettings | Set-JsonContentLF -path $repoSettingsFile + if ($addRepoSettings.Keys.Count) { + Add-PropertiesToJsonFile -path $repoSettingsFile -properties $addRepoSettings + } + + RefreshToken -repository $repository + + invoke-git add * + invoke-git commit --allow-empty -m 'init' + invoke-git branch -M $branch + if ($githubOwner) { + if ($github) { + invoke-git remote set-url origin "https://$($githubOwner)@github.com/$repository.git" + } else { + invoke-git remote set-url origin "https://github.com/$repository" + } + } + invoke-git push --set-upstream origin $branch + if (!$github) { + Start-Process "https://github.com/$repository/actions" + } + Start-Sleep -seconds 10 +} + +function Pull { + Param( + [string] $branch = "main" + ) + + invoke-git pull origin $branch +} + +function CommitAndPush { + Param( + [string] $commitMessage = "commitmessage", + [switch] $wait + ) + + if (!$repository) { + $repository = $defaultRepository + } + + RefreshToken -repository $repository + + if ($wait) { + $headers = GetHeaders -token $ENV:GH_TOKEN + Write-Host "Get Previous runs" + $url = "https://api.github.com/repos/$repository/actions/runs" + $previousrunids = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Select-Object -ExpandProperty id + if ($previousrunids) { + Write-Host "Previous runs: $($previousrunids -join ', ')" + } + else { + Write-Host "No previous runs found" + } + } + invoke-git add * + invoke-git commit --allow-empty -m "'$commitMessage'" + invoke-git push + if ($wait) { + while ($true) { + Start-Sleep -Seconds 10 + $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Where-Object { $previousrunids -notcontains $_.id } + if ($run) { + break + } + Write-Host "Run not started, waiting..." + } + WaitWorkflow -repository $repository -runid $run.id + $run + } +} + +function MergePRandPull { + Param( + [string] $repository, + [string] $branch = "main", + [switch] $wait + ) + + if (!$repository) { + $repository = $defaultRepository + } + + Write-Host "Get Previous runs" + RefreshToken -repository $repository + $headers = GetHeaders -token $ENV:GH_TOKEN -repository $repository + $url = "https://api.github.com/repos/$repository/actions/runs" + $previousrunids = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Select-Object -ExpandProperty id + if ($previousrunids) { + Write-Host "Previous runs: $($previousrunids -join ', ')" + } + else { + Write-Host "No previous runs found" + } + + $phs = @(invoke-gh -returnValue pr list --repo $repository) + if ($phs.Count -eq 0) { + throw "No Pull Request was created" + } + elseif ($phs.Count -gt 1) { + throw "More than one Pull Request exists" + } + $prid = $phs.Split("`t")[0] + Write-Host -ForegroundColor Yellow "`nMerge Pull Request $prid into repository $repository" + invoke-gh pr merge $prid --squash --delete-branch --repo $repository | Out-Host + while ($true) { + Start-Sleep -Seconds 10 + $run = ((InvokeWebRequest -Method Get -Headers $headers -Uri $url).Content | ConvertFrom-Json).workflow_runs | Where-Object { $_.event -eq 'push' } | Where-Object { $previousrunids -notcontains $_.id } + if ($run) { + break + } + Write-Host "Run not started, waiting..." + } + if ($wait) { + WaitWorkflow -repository $repository -runid $run.id + } + Write-Host "Merge commit run: $($run.id)" + $run + Pull -branch $branch +} + +function RemoveRepository { + Param( + [string] $repository, + [string] $path = "" + ) + + if (!$repository) { + $repository = $defaultRepository + } + if ($repository) { + try { + $owner = $repository.Split("/")[0] + Write-Host -ForegroundColor Yellow "`nRemoving repository $repository" + # Remove all packages belonging to the repository + $ownerType = invoke-gh api users/$owner --jq .type -silent -returnValue + if ($ownerType -eq 'User') { + # Package belongs to a user + $ownerStr = "users/$owner" + } + else { + # Package belongs to an organization + $ownerStr = "orgs/$owner" + } + @((invoke-gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /$ownerStr/packages?package_type=nuget -silent -returnvalue -ErrorAction SilentlyContinue | ConvertFrom-Json)) | Where-Object { $_.PSObject.Properties.Name -eq 'repository' } | Where-Object { $_.repository.full_name -eq $repository } | ForEach-Object { + Write-Host "+ package $($_.name)" + # Pipe empty string into GH API --METHOD DELETE due to https://github.com/cli/cli/issues/3937 + '' | invoke-gh api --method DELETE -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /$ownerStr/packages/nuget/$($_.name) --input + } + } + catch { + Write-Host -ForegroundColor Red "Error removing packages" + Write-Host -ForegroundColor Red $_.Exception.Message + } + invoke-gh repo delete $repository --yes | Out-Host + } + + if ($path) { + if (-not $path.StartsWith("$([System.IO.Path]::GetTempPath())",[StringComparison]::InvariantCultureIgnoreCase)) { + throw "$path is not temppath" + } + else { + Set-Location ([System.IO.Path]::GetTempPath()) + Remove-Item $path -Recurse -Force + } + } +} + +function Test-LogContainsFromRun { + Param( + [string] $repository, + [string] $runid, + [string] $jobName, + [string] $stepName, + [string] $expectedText, + [switch] $isRegEx + ) + + DownloadWorkflowLog -repository $repository -runid $runid -path 'logs' + try { + # Log format changes are rolling out on GitHub Actions, we have to support both + $oldStepLogFile = "logs/$jobName/*_$stepName.txt" + $newJobLogFile = "logs/*_$jobName.txt" + if (Test-Path -Path $oldStepLogFile) { + $runPipelineLog = Get-Content -Path (Get-Item $oldStepLogFile).FullName -encoding utf8 -raw + } + else { + $jobLog = Get-Content -Path (Get-Item $newJobLogFile).FullName -encoding utf8 + $emit = $false + $runPipelineLog = @($jobLog | ForEach-Object { + if ($emit -and $_ -like "*##[[]group]Run *@*") { + Write-Host -ForegroundColor Yellow "Foundend $_" + $emit = $false + } + elseif ($_ -like "*##[[]group]Run *$StepName@*") { + Write-Host -ForegroundColor Yellow "Foundstart $_" + $emit = $true + } + else { + Write-Host -ForegroundColor Gray $_ + } + if ($emit) { $_ } + }) -join "`n" + } + + if ($isRegEx) { + $found = $runPipelineLog -match $expectedText + return $Matches + } + else { + $found = $runPipelineLog.indexOf($expectedText, [System.StringComparison]::OrdinalIgnoreCase) -ne -1 + } + + if ($found) { + Write-Host "'$expectedText' found in the log for '$jobName -> $stepName' as expected" + } + else { + throw "Expected to find '$expectedText' in the log for '$jobName -> $stepName', but did not find it" + } + } + finally { + Remove-Item -Path 'logs' -Recurse -Force + } +} + +. (Join-Path $PSScriptRoot "Workflows\RunAddExistingAppOrTestApp.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunCICD.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunCreateApp.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunDeployReferenceDocumentation.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunCreateOnlineDevelopmentEnvironment.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunCreateRelease.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunCreateTestApp.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunIncrementVersionNumber.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunPublishToEnvironment.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunUpdateAlGoSystemFiles.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunTestCurrent.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunTestNextMinor.ps1") +. (Join-Path $PSScriptRoot "Workflows\RunTestNextMajor.ps1") + +. (Join-Path $PSScriptRoot "Test-Functions.ps1") From e1e45090fd8f856e8ec746d3f967cd03446e36dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:24:40 +0000 Subject: [PATCH 12/12] Disable FederatedCredentials test in E2E workflow matrix Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- .github/workflows/E2E.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/E2E.yaml b/.github/workflows/E2E.yaml index f1a038c345..e5424ad84f 100644 --- a/.github/workflows/E2E.yaml +++ b/.github/workflows/E2E.yaml @@ -231,6 +231,8 @@ jobs: $scenariosFilter = "$($env:_scenariosFilter)" -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } $allScenarios = @(Get-ChildItem -Path (Join-Path $ENV:GITHUB_WORKSPACE "e2eTests/scenarios/*/runtest.ps1") | ForEach-Object { $_.Directory.Name }) $filteredScenarios = $allScenarios | Where-Object { $scenario = $_; $scenariosFilter | ForEach-Object { $scenario -like $_ } } + # Temporarily disable FederatedCredentials test due to Azure resource migration work + $filteredScenarios = $filteredScenarios | Where-Object { $_ -ne 'FederatedCredentials' } $scenariosJson = @{ "matrix" = @{