From 3c7d0ba550dc89c0f80ef2deb1f934563c9fff91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 12:51:38 +0000 Subject: [PATCH 1/7] Initial plan From 7e72f93c37931c746fe0d7b1e8e4edcd6830334b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 12:59:27 +0000 Subject: [PATCH 2/7] Implement fix for custom job removal issue Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 21 +- Tests/CheckForUpdates.Action.Test.ps1 | 69 +++++- Tests/CustomJobRemoval.Test.ps1 | 240 ++++++++++++++++++++ 3 files changed, 320 insertions(+), 10 deletions(-) create mode 100644 Tests/CustomJobRemoval.Test.ps1 diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 01335d0ef..b7142f1ad 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -254,8 +254,25 @@ foreach($checkfile in $checkfiles) { if ($dstFileExists) { if ($type -eq 'workflow') { - Write-Host "Apply customizations from current repository: $dstFile" - [Yaml]::ApplyCustomizations([ref] $srcContent, $dstFile) + # Determine if current repository is a final repository (has templateUrl pointing to another repo) + # Final repositories should not have custom jobs applied to prevent persistence of removed template jobs + $currentRepoReference = $env:GITHUB_REPOSITORY + $isFinalRepository = $false + + if ($repoSettings.templateUrl) { + # Extract repository reference from templateUrl (e.g., "microsoft/AL-Go-PTE" from "https://github.com/microsoft/AL-Go-PTE@main") + $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] + $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' + $isFinalRepository = $templateRepoReference -ne $currentRepoReference + } + + if ($isFinalRepository) { + Write-Host "Skipping custom jobs from current repository (final repository using template: $($repoSettings.templateUrl)): $dstFile" + } + else { + Write-Host "Apply customizations from current repository: $dstFile" + [Yaml]::ApplyCustomizations([ref] $srcContent, $dstFile) + } } # file exists, compare and add to $updateFiles if different diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 3b8cd8379..ffa1760ae 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -134,14 +134,67 @@ Describe "CheckForUpdates Action Tests" { ($yaml.content -join "`r`n") | Should -be ($customizedYaml.content -join "`r`n") } - It 'Test that Update AL-Go System Files uses fixes runs-on' { - . (Join-Path $scriptRoot "yamlclass.ps1") - - $updateYamlFile = Join-Path $scriptRoot "..\..\Templates\Per Tenant Extension\.github\workflows\UpdateGitHubGoSystemFiles.yaml" - $updateYaml = [Yaml]::Load($updateYamlFile) - $updateYaml.content | Where-Object { $_ -like '*runs-on:*' } | ForEach-Object { - $_.Trim() | Should -Be 'runs-on: windows-latest' -Because "Expected 'runs-on: windows-latest', in order to hardcode runner to windows-latest, but got $_" - } + It 'Test that Update AL-Go System Files uses fixes runs-on' { + . (Join-Path $scriptRoot "yamlclass.ps1") + + $updateYamlFile = Join-Path $scriptRoot "..\..\Templates\Per Tenant Extension\.github\workflows\UpdateGitHubGoSystemFiles.yaml" + $updateYaml = [Yaml]::Load($updateYamlFile) + $updateYaml.content | Where-Object { $_ -like '*runs-on:*' } | ForEach-Object { + $_.Trim() | Should -Be 'runs-on: windows-latest' -Because "Expected 'runs-on: windows-latest', in order to hardcode runner to windows-latest, but got $_" + } + } + + It 'Test final repository detection logic' { + # Test the repository detection logic used to determine if custom jobs should be applied + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") + + # Mock environment variables + $env:GITHUB_REPOSITORY = "testowner/testrepo" + + # Test scenario 1: Final repository using template + $repoSettings = @{ + templateUrl = "https://github.com/testowner/template-repo@main" + } + + # Extract repository reference logic (mirroring the actual code) + $currentRepoReference = $env:GITHUB_REPOSITORY + $isFinalRepository = $false + + if ($repoSettings.templateUrl) { + $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] + $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' + $isFinalRepository = $templateRepoReference -ne $currentRepoReference + } + + $isFinalRepository | Should -Be $true -Because "Repository using template should be detected as final repository" + + # Test scenario 2: Template repository (no templateUrl) + $repoSettings = @{} + + $isFinalRepository = $false + if ($repoSettings.templateUrl) { + $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] + $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' + $isFinalRepository = $templateRepoReference -ne $currentRepoReference + } + + $isFinalRepository | Should -Be $false -Because "Repository without templateUrl should not be detected as final repository" + + # Test scenario 3: Repository referencing itself (edge case) + $repoSettings = @{ + templateUrl = "https://github.com/testowner/testrepo@main" + } + + $isFinalRepository = $false + if ($repoSettings.templateUrl) { + $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] + $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' + $isFinalRepository = $templateRepoReference -ne $currentRepoReference + } + + $isFinalRepository | Should -Be $false -Because "Repository referencing itself should not be detected as final repository" } } diff --git a/Tests/CustomJobRemoval.Test.ps1 b/Tests/CustomJobRemoval.Test.ps1 new file mode 100644 index 000000000..561e4d048 --- /dev/null +++ b/Tests/CustomJobRemoval.Test.ps1 @@ -0,0 +1,240 @@ +Get-Module TestActionsHelper | Remove-Module -Force +Import-Module (Join-Path $PSScriptRoot 'TestActionsHelper.psm1') +$errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 + +Describe "Custom Job Removal Tests" { + BeforeAll { + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + Import-Module (Join-Path $scriptRoot "..\Github-Helper.psm1") -DisableNameChecking -Force + . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") + . (Join-Path -Path $scriptRoot -ChildPath "yamlclass.ps1") + + # Create temporary directory for test files + $testDir = Join-Path $PSScriptRoot "temp_custom_job_test" + if (Test-Path $testDir) { + Remove-Item -Path $testDir -Recurse -Force + } + New-Item -Path $testDir -ItemType Directory -Force | Out-Null + } + + AfterAll { + # Clean up test directory + $testDir = Join-Path $PSScriptRoot "temp_custom_job_test" + if (Test-Path $testDir) { + Remove-Item -Path $testDir -Recurse -Force + } + } + + It 'Custom jobs should not be applied from final repositories' { + $testDir = Join-Path $PSScriptRoot "temp_custom_job_test" + + # Create a mock template CICD workflow (base workflow) + $templateWorkflow = @( + "name: 'CI/CD'", + "on:", + " workflow_dispatch:", + "jobs:", + " Initialization:", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Initialize", + " run: echo 'Initializing'", + " Build:", + " needs: [ Initialization ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Build", + " run: echo 'Building'", + " PostProcess:", + " needs: [ Initialization, Build ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: PostProcess", + " run: echo 'PostProcessing'" + ) + + # Create a final repository workflow with custom jobs (simulating a repository that uses a template) + $finalRepoWorkflow = @( + "name: 'CI/CD'", + "on:", + " workflow_dispatch:", + "jobs:", + " Initialization:", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Initialize", + " run: echo 'Initializing'", + " CustomJob-ShouldNotPersist:", + " needs: [ Initialization ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Custom Step", + " run: echo 'This custom job should not persist'", + " Build:", + " needs: [ Initialization, CustomJob-ShouldNotPersist ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Build", + " run: echo 'Building'", + " PostProcess:", + " needs: [ Initialization, Build ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: PostProcess", + " run: echo 'PostProcessing'" + ) + + # Save test files + $templateFile = Join-Path $testDir "template_cicd.yaml" + $finalRepoFile = Join-Path $testDir "final_repo_cicd.yaml" + + $templateWorkflow -join "`n" | Set-Content -Path $templateFile -Encoding UTF8 + $finalRepoWorkflow -join "`n" | Set-Content -Path $finalRepoFile -Encoding UTF8 + + # Mock environment and repo settings for final repository + $env:GITHUB_REPOSITORY = "testowner/final-repo" + $repoSettings = @{ + templateUrl = "https://github.com/testowner/template-repo@main" + } + + # Simulate the logic from CheckForUpdates.ps1 + $srcContent = Get-Content -Path $templateFile -Raw + $dstFileExists = Test-Path -Path $finalRepoFile + $type = 'workflow' + + # Apply the final repository detection logic + $currentRepoReference = $env:GITHUB_REPOSITORY + $isFinalRepository = $false + + if ($repoSettings.templateUrl) { + $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] + $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' + $isFinalRepository = $templateRepoReference -ne $currentRepoReference + } + + # Test that final repository is correctly detected + $isFinalRepository | Should -Be $true + + # Apply customizations based on repository type + if ($dstFileExists -and $type -eq 'workflow') { + if (-not $isFinalRepository) { + [Yaml]::ApplyCustomizations([ref] $srcContent, $finalRepoFile) + } + } + + # Verify that custom jobs were NOT applied (srcContent should not contain CustomJob-ShouldNotPersist) + $srcContent | Should -Not -Match "CustomJob-ShouldNotPersist" + $srcContent | Should -Not -Match "This custom job should not persist" + + # Verify that the base template structure is preserved + $srcContent | Should -Match "Initialization:" + $srcContent | Should -Match "Build:" + $srcContent | Should -Match "PostProcess:" + } + + It 'Custom jobs should be applied from template repositories' { + $testDir = Join-Path $PSScriptRoot "temp_custom_job_test" + + # Create a mock template CICD workflow (base workflow) + $templateWorkflow = @( + "name: 'CI/CD'", + "on:", + " workflow_dispatch:", + "jobs:", + " Initialization:", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Initialize", + " run: echo 'Initializing'", + " Build:", + " needs: [ Initialization ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Build", + " run: echo 'Building'", + " PostProcess:", + " needs: [ Initialization, Build ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: PostProcess", + " run: echo 'PostProcessing'" + ) + + # Create a template repository workflow with custom jobs + $templateRepoWorkflow = @( + "name: 'CI/CD'", + "on:", + " workflow_dispatch:", + "jobs:", + " Initialization:", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Initialize", + " run: echo 'Initializing'", + " CustomJob-Template:", + " needs: [ Initialization ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Template Custom Step", + " run: echo 'This is a template custom job'", + " Build:", + " needs: [ Initialization, CustomJob-Template ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Build", + " run: echo 'Building'", + " PostProcess:", + " needs: [ Initialization, Build ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: PostProcess", + " run: echo 'PostProcessing'" + ) + + # Save test files + $templateFile = Join-Path $testDir "template_cicd2.yaml" + $templateRepoFile = Join-Path $testDir "template_repo_cicd.yaml" + + $templateWorkflow -join "`n" | Set-Content -Path $templateFile -Encoding UTF8 + $templateRepoWorkflow -join "`n" | Set-Content -Path $templateRepoFile -Encoding UTF8 + + # Mock environment and repo settings for template repository (no templateUrl) + $env:GITHUB_REPOSITORY = "testowner/template-repo" + $repoSettings = @{} # No templateUrl - this is a template repository + + # Simulate the logic from CheckForUpdates.ps1 + $srcContent = Get-Content -Path $templateFile -Raw + $dstFileExists = Test-Path -Path $templateRepoFile + $type = 'workflow' + + # Apply the final repository detection logic + $currentRepoReference = $env:GITHUB_REPOSITORY + $isFinalRepository = $false + + if ($repoSettings.templateUrl) { + $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] + $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' + $isFinalRepository = $templateRepoReference -ne $currentRepoReference + } + + # Test that template repository is correctly detected + $isFinalRepository | Should -Be $false + + # Apply customizations based on repository type + if ($dstFileExists -and $type -eq 'workflow') { + if (-not $isFinalRepository) { + [Yaml]::ApplyCustomizations([ref] $srcContent, $templateRepoFile) + } + } + + # Verify that custom jobs WERE applied (srcContent should contain CustomJob-Template) + $srcContent | Should -Match "CustomJob-Template" + $srcContent | Should -Match "This is a template custom job" + + # Verify that the base template structure is preserved + $srcContent | Should -Match "Initialization:" + $srcContent | Should -Match "Build:" + $srcContent | Should -Match "PostProcess:" + } +} \ No newline at end of file From 01b28124863ab2cf7ee998c98c12f793faab5d65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:11:02 +0000 Subject: [PATCH 3/7] Update documentation to reflect custom job behavior changes Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- Scenarios/UpdateAlGoSystemFiles.md | 4 ++++ Scenarios/settings.md | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Scenarios/UpdateAlGoSystemFiles.md b/Scenarios/UpdateAlGoSystemFiles.md index 0ccc479be..5f158f7ce 100644 --- a/Scenarios/UpdateAlGoSystemFiles.md +++ b/Scenarios/UpdateAlGoSystemFiles.md @@ -24,6 +24,10 @@ 1. By default, this workflow will apply any updates to the **workflow files (in .github\\workflows)** or **system scripts (in .AL-Go)** from the template repository used to spin up the repository. If you want to change branch or template Url, you can specify the `templateUrl@branch` when you run the workflow. +1. **Custom jobs behavior**: When updating system files, custom jobs are handled differently based on repository type: + - **Template repositories** (without `templateUrl` setting): Custom jobs are preserved and applied + - **Final repositories** (with `templateUrl` pointing to another repository): Custom jobs from the current repository are NOT applied to prevent persistence of jobs removed from the template + ______________________________________________________________________ [back](../README.md) diff --git a/Scenarios/settings.md b/Scenarios/settings.md index 564ba37fe..27cc8f89b 100644 --- a/Scenarios/settings.md +++ b/Scenarios/settings.md @@ -408,7 +408,10 @@ In the `needs` property, you specify which jobs should be complete before this j strategy: ${{ fromJson(needs.Initialization.outputs.environmentsMatrixJson) }} ``` -Custom jobs will be preserved when running Update AL-Go System Files. +Custom jobs will be preserved when running Update AL-Go System Files, with the following behavior: + +- **Template repositories** (repositories without a `templateUrl` setting): Custom jobs are preserved and applied during updates +- **Final repositories** (repositories with a `templateUrl` pointing to another repository): Custom jobs from the current repository are NOT applied during updates to prevent persistence of jobs removed from the template **Note** that installing [apps from the GitHub marketplace](https://github.com/marketplace?type=apps) might require you to add custom jobs or steps to some of the workflows to get the right integration. In custom jobs, you can use any [actions from the GitHub marketplace](https://github.com/marketplace?type=actions). From f371a4131cbf2056d92d32ba80517d7b8ce41dc8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:19:22 +0000 Subject: [PATCH 4/7] Remove all trailing whitespaces from PowerShell files Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 4 +- Tests/CheckForUpdates.Action.Test.ps1 | 426 ++++++++++---------- Tests/CustomJobRemoval.Test.ps1 | 50 +-- 3 files changed, 240 insertions(+), 240 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index b7142f1ad..829969576 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -258,14 +258,14 @@ foreach($checkfile in $checkfiles) { # Final repositories should not have custom jobs applied to prevent persistence of removed template jobs $currentRepoReference = $env:GITHUB_REPOSITORY $isFinalRepository = $false - + if ($repoSettings.templateUrl) { # Extract repository reference from templateUrl (e.g., "microsoft/AL-Go-PTE" from "https://github.com/microsoft/AL-Go-PTE@main") $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' $isFinalRepository = $templateRepoReference -ne $currentRepoReference } - + if ($isFinalRepository) { Write-Host "Skipping custom jobs from current repository (final repository using template: $($repoSettings.templateUrl)): $dstFile" } diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index ffa1760ae..f3a436621 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -1,139 +1,139 @@ -Get-Module TestActionsHelper | Remove-Module -Force -Import-Module (Join-Path $PSScriptRoot 'TestActionsHelper.psm1') -$errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 - -Describe "CheckForUpdates Action Tests" { - BeforeAll { - $actionName = "CheckForUpdates" - $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'actionScript', Justification = 'False positive.')] - $actionScript = GetActionScript -scriptRoot $scriptRoot -scriptName "$actionName.ps1" - } - - It 'Compile Action' { - Invoke-Expression $actionScript - } - - It 'Test action.yaml matches script' { - $outputs = [ordered]@{ - } - YamlTest -scriptRoot $scriptRoot -actionName $actionName -actionScript $actionScript -outputs $outputs - } - - It 'Test YamlClass' { - . (Join-Path $scriptRoot "yamlclass.ps1") - $yaml = [Yaml]::load((Join-Path $PSScriptRoot 'YamlSnippet.txt')) - - # Yaml file should have 77 entries - $yaml.content.Count | Should -be 73 - - $start = 0; $count = 0 - # Locate lines for permissions section (including permissions: line) - $yaml.Find('permissions:', [ref] $start, [ref] $count) | Should -be $true - $start | Should -be 17 - $count | Should -be 5 - - # Locate lines for permissions section (excluding permissions: line) - $yaml.Find('permissions:/', [ref] $start, [ref] $count) | Should -be $true - $start | Should -be 18 - $count | Should -be 4 - - # Get Yaml class for permissions section (excluding permissions: line) - $yaml.Get('permissions:/').content | ForEach-Object { $_ | Should -not -belike ' *' } - - # Locate section called permissionos (should return false) - $yaml.Find('permissionos:', [ref] $start, [ref] $count) | Should -Not -be $true - - # Check checkout step - ($yaml.Get('jobs:/Initialization:/steps:/- name: Checkout').content -join '') | Should -be "- name: Checkout uses: actions/checkout@v4 with: lfs: true" - - # Get Shell line in read Settings step - ($yaml.Get('jobs:/Initialization:/steps:/- name: Read settings/with:/shell:').content -join '') | Should -be "shell: powershell" - - # Get Jobs section (without the jobs: line) - $jobsYaml = $yaml.Get('jobs:/') - - # Locate CheckForUpdates - $jobsYaml.Find('CheckForUpdates:', [ref] $start, [ref] $count) | Should -be $true - $start | Should -be 23 - $count | Should -be 19 - - # Replace all occurances of 'shell: powershell' with 'shell: pwsh' - $yaml.ReplaceAll('shell: powershell','shell: pwsh') - $yaml.content[46].Trim() | Should -be 'shell: pwsh' - - # Replace Permissions - $yaml.Replace('Permissions:/',@('contents: write','actions: read')) - $yaml.content[44].Trim() | Should -be 'shell: pwsh' - $yaml.content.Count | Should -be 71 - - # Get Jobs section (without the jobs: line) - $jobsYaml = $yaml.Get('jobs:/') - ($jobsYaml.Get('Initialization:/steps:/- name: Read settings/with:/shell:').content -join '') | Should -be "shell: pwsh" - } - - It 'Test YamlClass Remove' { - . (Join-Path $scriptRoot "yamlclass.ps1") - - $yamlSnippet = @( - "permissions:", - " contents: read", - " actions: read", - " pull-requests: write", - " checks: write" - ) - - $permissionsYaml = [Yaml]::new($yamlSnippet) - - $permissionsContent = $permissionsYaml.Get('permissions:/') - $permissionsContent.content.Count | Should -be 4 - $permissionsContent.Remove(1, 0) # Remove nothing - $permissionsContent.content.Count | Should -be 4 - $permissionsContent.content[0].Trim() | Should -be 'contents: read' - $permissionsContent.content[1].Trim() | Should -be 'actions: read' - $permissionsContent.content[2].Trim() | Should -be 'pull-requests: write' - $permissionsContent.content[3].Trim() | Should -be 'checks: write' - - $permissionsContent = $permissionsYaml.Get('permissions:/') - $permissionsContent.content.Count | Should -be 4 - $permissionsContent.Remove(0, 3) # Remove first 3 lines - $permissionsContent.content.Count | Should -be 1 - $permissionsContent.content[0].Trim() | Should -be 'checks: write' - - $permissionsContent = $permissionsYaml.Get('permissions:/') - $permissionsContent.content.Count | Should -be 4 - $permissionsContent.Remove(2, 1) # Remove only the 3rd line - $permissionsContent.content.Count | Should -be 3 - $permissionsContent.content[0].Trim() | Should -be 'contents: read' - $permissionsContent.content[1].Trim() | Should -be 'actions: read' - $permissionsContent.content[2].Trim() | Should -be 'checks: write' - - $permissionsContent = $permissionsYaml.Get('permissions:/') - $permissionsContent.content.Count | Should -be 4 - $permissionsContent.Remove(2, 4) # Remove more than the number of lines - $permissionsContent.content.Count | Should -be 2 # Only the first two lines should remain - $permissionsContent.content[0].Trim() | Should -be 'contents: read' - $permissionsContent.content[1].Trim() | Should -be 'actions: read' - } - - It 'Test YamlClass Customizations' { - . (Join-Path $scriptRoot "yamlclass.ps1") - - $customizedYaml = [Yaml]::load((Join-Path $PSScriptRoot 'CustomizedYamlSnippet.txt')) - $yaml = [Yaml]::load((Join-Path $PSScriptRoot 'YamlSnippet.txt')) - - # Get Custom jobs from yaml - $customJobs = $customizedYaml.GetCustomJobsFromYaml('CustomJob*') - $customJobs | Should -Not -BeNullOrEmpty - $customJobs.Count | Should -be 1 - - # Apply Custom jobs and steps to yaml - $yaml.AddCustomJobsToYaml($customJobs) - - # Check if new yaml content is equal to customized yaml content - ($yaml.content -join "`r`n") | Should -be ($customizedYaml.content -join "`r`n") - } - +Get-Module TestActionsHelper | Remove-Module -Force +Import-Module (Join-Path $PSScriptRoot 'TestActionsHelper.psm1') +$errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 + +Describe "CheckForUpdates Action Tests" { + BeforeAll { + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'actionScript', Justification = 'False positive.')] + $actionScript = GetActionScript -scriptRoot $scriptRoot -scriptName "$actionName.ps1" + } + + It 'Compile Action' { + Invoke-Expression $actionScript + } + + It 'Test action.yaml matches script' { + $outputs = [ordered]@{ + } + YamlTest -scriptRoot $scriptRoot -actionName $actionName -actionScript $actionScript -outputs $outputs + } + + It 'Test YamlClass' { + . (Join-Path $scriptRoot "yamlclass.ps1") + $yaml = [Yaml]::load((Join-Path $PSScriptRoot 'YamlSnippet.txt')) + + # Yaml file should have 77 entries + $yaml.content.Count | Should -be 73 + + $start = 0; $count = 0 + # Locate lines for permissions section (including permissions: line) + $yaml.Find('permissions:', [ref] $start, [ref] $count) | Should -be $true + $start | Should -be 17 + $count | Should -be 5 + + # Locate lines for permissions section (excluding permissions: line) + $yaml.Find('permissions:/', [ref] $start, [ref] $count) | Should -be $true + $start | Should -be 18 + $count | Should -be 4 + + # Get Yaml class for permissions section (excluding permissions: line) + $yaml.Get('permissions:/').content | ForEach-Object { $_ | Should -not -belike ' *' } + + # Locate section called permissionos (should return false) + $yaml.Find('permissionos:', [ref] $start, [ref] $count) | Should -Not -be $true + + # Check checkout step + ($yaml.Get('jobs:/Initialization:/steps:/- name: Checkout').content -join '') | Should -be "- name: Checkout uses: actions/checkout@v4 with: lfs: true" + + # Get Shell line in read Settings step + ($yaml.Get('jobs:/Initialization:/steps:/- name: Read settings/with:/shell:').content -join '') | Should -be "shell: powershell" + + # Get Jobs section (without the jobs: line) + $jobsYaml = $yaml.Get('jobs:/') + + # Locate CheckForUpdates + $jobsYaml.Find('CheckForUpdates:', [ref] $start, [ref] $count) | Should -be $true + $start | Should -be 23 + $count | Should -be 19 + + # Replace all occurances of 'shell: powershell' with 'shell: pwsh' + $yaml.ReplaceAll('shell: powershell','shell: pwsh') + $yaml.content[46].Trim() | Should -be 'shell: pwsh' + + # Replace Permissions + $yaml.Replace('Permissions:/',@('contents: write','actions: read')) + $yaml.content[44].Trim() | Should -be 'shell: pwsh' + $yaml.content.Count | Should -be 71 + + # Get Jobs section (without the jobs: line) + $jobsYaml = $yaml.Get('jobs:/') + ($jobsYaml.Get('Initialization:/steps:/- name: Read settings/with:/shell:').content -join '') | Should -be "shell: pwsh" + } + + It 'Test YamlClass Remove' { + . (Join-Path $scriptRoot "yamlclass.ps1") + + $yamlSnippet = @( + "permissions:", + " contents: read", + " actions: read", + " pull-requests: write", + " checks: write" + ) + + $permissionsYaml = [Yaml]::new($yamlSnippet) + + $permissionsContent = $permissionsYaml.Get('permissions:/') + $permissionsContent.content.Count | Should -be 4 + $permissionsContent.Remove(1, 0) # Remove nothing + $permissionsContent.content.Count | Should -be 4 + $permissionsContent.content[0].Trim() | Should -be 'contents: read' + $permissionsContent.content[1].Trim() | Should -be 'actions: read' + $permissionsContent.content[2].Trim() | Should -be 'pull-requests: write' + $permissionsContent.content[3].Trim() | Should -be 'checks: write' + + $permissionsContent = $permissionsYaml.Get('permissions:/') + $permissionsContent.content.Count | Should -be 4 + $permissionsContent.Remove(0, 3) # Remove first 3 lines + $permissionsContent.content.Count | Should -be 1 + $permissionsContent.content[0].Trim() | Should -be 'checks: write' + + $permissionsContent = $permissionsYaml.Get('permissions:/') + $permissionsContent.content.Count | Should -be 4 + $permissionsContent.Remove(2, 1) # Remove only the 3rd line + $permissionsContent.content.Count | Should -be 3 + $permissionsContent.content[0].Trim() | Should -be 'contents: read' + $permissionsContent.content[1].Trim() | Should -be 'actions: read' + $permissionsContent.content[2].Trim() | Should -be 'checks: write' + + $permissionsContent = $permissionsYaml.Get('permissions:/') + $permissionsContent.content.Count | Should -be 4 + $permissionsContent.Remove(2, 4) # Remove more than the number of lines + $permissionsContent.content.Count | Should -be 2 # Only the first two lines should remain + $permissionsContent.content[0].Trim() | Should -be 'contents: read' + $permissionsContent.content[1].Trim() | Should -be 'actions: read' + } + + It 'Test YamlClass Customizations' { + . (Join-Path $scriptRoot "yamlclass.ps1") + + $customizedYaml = [Yaml]::load((Join-Path $PSScriptRoot 'CustomizedYamlSnippet.txt')) + $yaml = [Yaml]::load((Join-Path $PSScriptRoot 'YamlSnippet.txt')) + + # Get Custom jobs from yaml + $customJobs = $customizedYaml.GetCustomJobsFromYaml('CustomJob*') + $customJobs | Should -Not -BeNullOrEmpty + $customJobs.Count | Should -be 1 + + # Apply Custom jobs and steps to yaml + $yaml.AddCustomJobsToYaml($customJobs) + + # Check if new yaml content is equal to customized yaml content + ($yaml.content -join "`r`n") | Should -be ($customizedYaml.content -join "`r`n") + } + It 'Test that Update AL-Go System Files uses fixes runs-on' { . (Join-Path $scriptRoot "yamlclass.ps1") @@ -149,115 +149,115 @@ Describe "CheckForUpdates Action Tests" { $actionName = "CheckForUpdates" $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") - + # Mock environment variables $env:GITHUB_REPOSITORY = "testowner/testrepo" - + # Test scenario 1: Final repository using template $repoSettings = @{ templateUrl = "https://github.com/testowner/template-repo@main" } - + # Extract repository reference logic (mirroring the actual code) $currentRepoReference = $env:GITHUB_REPOSITORY $isFinalRepository = $false - + if ($repoSettings.templateUrl) { $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' $isFinalRepository = $templateRepoReference -ne $currentRepoReference } - + $isFinalRepository | Should -Be $true -Because "Repository using template should be detected as final repository" - + # Test scenario 2: Template repository (no templateUrl) $repoSettings = @{} - + $isFinalRepository = $false if ($repoSettings.templateUrl) { $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' $isFinalRepository = $templateRepoReference -ne $currentRepoReference } - + $isFinalRepository | Should -Be $false -Because "Repository without templateUrl should not be detected as final repository" - + # Test scenario 3: Repository referencing itself (edge case) $repoSettings = @{ templateUrl = "https://github.com/testowner/testrepo@main" } - + $isFinalRepository = $false if ($repoSettings.templateUrl) { $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' $isFinalRepository = $templateRepoReference -ne $currentRepoReference } - + $isFinalRepository | Should -Be $false -Because "Repository referencing itself should not be detected as final repository" - } -} - -Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" { - BeforeAll { - $actionName = "CheckForUpdates" - $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve - Import-Module (Join-Path $scriptRoot "..\Github-Helper.psm1") -DisableNameChecking -Force - . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'actionScript', Justification = 'False positive.')] - $tmpSrcFile = Join-Path $PSScriptRoot "tempSrcFile.json" - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'actionScript', Justification = 'False positive.')] - $tmpDstFile = Join-Path $PSScriptRoot "tempDestFile.json" - } - - AfterEach { - # Clean up temporary files - if (Test-Path $tmpSrcFile) { - Remove-Item -Path $tmpSrcFile -Force - } - if (Test-Path $tmpDstFile) { - Remove-Item -Path $tmpDstFile -Force - } - } - - It 'GetModifiedSettingsContent returns correct content when destination file is not empty' { - # Create settings files with the content - @{ "`$schema" = "someSchema"; "srcSetting" = "value1" } | ConvertTo-Json -Depth 10 | Out-File -FilePath $tmpSrcFile -Force - @{ "setting1" = "value2" } | ConvertTo-Json -Depth 10 | Out-File -FilePath $tmpDstFile -Force - - $modifiedContentJson = GetModifiedSettingsContent -srcSettingsFile $tmpSrcFile -dstSettingsFile $tmpDstFile - - $modifiedContent = $modifiedContentJson | ConvertFrom-Json - $modifiedContent | Should -Not -BeNullOrEmpty - $modifiedContent.PSObject.Properties.Name.Count | Should -Be 2 # setting1 and $schema - $modifiedContent."setting1" | Should -Be "value2" - $modifiedContent."`$schema" | Should -Be "someSchema" - } - - It 'GetModifiedSettingsContent returns correct content when destination file is empty' { - # Create only the source file - @{ "`$schema" = "someSchema"; "srcSetting" = "value1" } | ConvertTo-Json -Depth 10 | Out-File -FilePath $tmpSrcFile -Force - '' | Out-File -FilePath $tmpDstFile -Force - $modifiedContentJson = GetModifiedSettingsContent -srcSettingsFile $tmpSrcFile -dstSettingsFile $tmpDstFile - - $modifiedContent = $modifiedContentJson | ConvertFrom-Json - $modifiedContent | Should -Not -BeNullOrEmpty - @($modifiedContent.PSObject.Properties.Name).Count | Should -Be 2 # srcSetting and $schema - $modifiedContent."`$schema" | Should -Be "someSchema" - $modifiedContent."srcSetting" | Should -Be "value1" - } - - It 'GetModifiedSettingsContent returns correct content when destination file does not exist' { - # Create only the source file - @{ "`$schema" = "someSchema"; "srcSetting" = "value1" } | ConvertTo-Json -Depth 10 | Out-File -FilePath $tmpSrcFile -Force - - Test-Path $tmpDstFile | Should -Be $false - $modifiedContentJson = GetModifiedSettingsContent -srcSettingsFile $tmpSrcFile -dstSettingsFile $tmpDstFile - - $modifiedContent = $modifiedContentJson | ConvertFrom-Json - $modifiedContent | Should -Not -BeNullOrEmpty - $modifiedContent.PSObject.Properties.Name.Count | Should -Be 2 # srcSetting and $schema - $modifiedContent."srcSetting" | Should -Be "value1" - $modifiedContent."`$schema" | Should -Be "someSchema" - } -} + } +} + +Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" { + BeforeAll { + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + Import-Module (Join-Path $scriptRoot "..\Github-Helper.psm1") -DisableNameChecking -Force + . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'actionScript', Justification = 'False positive.')] + $tmpSrcFile = Join-Path $PSScriptRoot "tempSrcFile.json" + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'actionScript', Justification = 'False positive.')] + $tmpDstFile = Join-Path $PSScriptRoot "tempDestFile.json" + } + + AfterEach { + # Clean up temporary files + if (Test-Path $tmpSrcFile) { + Remove-Item -Path $tmpSrcFile -Force + } + if (Test-Path $tmpDstFile) { + Remove-Item -Path $tmpDstFile -Force + } + } + + It 'GetModifiedSettingsContent returns correct content when destination file is not empty' { + # Create settings files with the content + @{ "`$schema" = "someSchema"; "srcSetting" = "value1" } | ConvertTo-Json -Depth 10 | Out-File -FilePath $tmpSrcFile -Force + @{ "setting1" = "value2" } | ConvertTo-Json -Depth 10 | Out-File -FilePath $tmpDstFile -Force + + $modifiedContentJson = GetModifiedSettingsContent -srcSettingsFile $tmpSrcFile -dstSettingsFile $tmpDstFile + + $modifiedContent = $modifiedContentJson | ConvertFrom-Json + $modifiedContent | Should -Not -BeNullOrEmpty + $modifiedContent.PSObject.Properties.Name.Count | Should -Be 2 # setting1 and $schema + $modifiedContent."setting1" | Should -Be "value2" + $modifiedContent."`$schema" | Should -Be "someSchema" + } + + It 'GetModifiedSettingsContent returns correct content when destination file is empty' { + # Create only the source file + @{ "`$schema" = "someSchema"; "srcSetting" = "value1" } | ConvertTo-Json -Depth 10 | Out-File -FilePath $tmpSrcFile -Force + '' | Out-File -FilePath $tmpDstFile -Force + $modifiedContentJson = GetModifiedSettingsContent -srcSettingsFile $tmpSrcFile -dstSettingsFile $tmpDstFile + + $modifiedContent = $modifiedContentJson | ConvertFrom-Json + $modifiedContent | Should -Not -BeNullOrEmpty + @($modifiedContent.PSObject.Properties.Name).Count | Should -Be 2 # srcSetting and $schema + $modifiedContent."`$schema" | Should -Be "someSchema" + $modifiedContent."srcSetting" | Should -Be "value1" + } + + It 'GetModifiedSettingsContent returns correct content when destination file does not exist' { + # Create only the source file + @{ "`$schema" = "someSchema"; "srcSetting" = "value1" } | ConvertTo-Json -Depth 10 | Out-File -FilePath $tmpSrcFile -Force + + Test-Path $tmpDstFile | Should -Be $false + $modifiedContentJson = GetModifiedSettingsContent -srcSettingsFile $tmpSrcFile -dstSettingsFile $tmpDstFile + + $modifiedContent = $modifiedContentJson | ConvertFrom-Json + $modifiedContent | Should -Not -BeNullOrEmpty + $modifiedContent.PSObject.Properties.Name.Count | Should -Be 2 # srcSetting and $schema + $modifiedContent."srcSetting" | Should -Be "value1" + $modifiedContent."`$schema" | Should -Be "someSchema" + } +} diff --git a/Tests/CustomJobRemoval.Test.ps1 b/Tests/CustomJobRemoval.Test.ps1 index 561e4d048..ca6c36bdc 100644 --- a/Tests/CustomJobRemoval.Test.ps1 +++ b/Tests/CustomJobRemoval.Test.ps1 @@ -9,7 +9,7 @@ Describe "Custom Job Removal Tests" { Import-Module (Join-Path $scriptRoot "..\Github-Helper.psm1") -DisableNameChecking -Force . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") . (Join-Path -Path $scriptRoot -ChildPath "yamlclass.ps1") - + # Create temporary directory for test files $testDir = Join-Path $PSScriptRoot "temp_custom_job_test" if (Test-Path $testDir) { @@ -28,7 +28,7 @@ Describe "Custom Job Removal Tests" { It 'Custom jobs should not be applied from final repositories' { $testDir = Join-Path $PSScriptRoot "temp_custom_job_test" - + # Create a mock template CICD workflow (base workflow) $templateWorkflow = @( "name: 'CI/CD'", @@ -53,7 +53,7 @@ Describe "Custom Job Removal Tests" { " - name: PostProcess", " run: echo 'PostProcessing'" ) - + # Create a final repository workflow with custom jobs (simulating a repository that uses a template) $finalRepoWorkflow = @( "name: 'CI/CD'", @@ -84,49 +84,49 @@ Describe "Custom Job Removal Tests" { " - name: PostProcess", " run: echo 'PostProcessing'" ) - + # Save test files $templateFile = Join-Path $testDir "template_cicd.yaml" $finalRepoFile = Join-Path $testDir "final_repo_cicd.yaml" - + $templateWorkflow -join "`n" | Set-Content -Path $templateFile -Encoding UTF8 $finalRepoWorkflow -join "`n" | Set-Content -Path $finalRepoFile -Encoding UTF8 - + # Mock environment and repo settings for final repository $env:GITHUB_REPOSITORY = "testowner/final-repo" $repoSettings = @{ templateUrl = "https://github.com/testowner/template-repo@main" } - + # Simulate the logic from CheckForUpdates.ps1 $srcContent = Get-Content -Path $templateFile -Raw $dstFileExists = Test-Path -Path $finalRepoFile $type = 'workflow' - + # Apply the final repository detection logic $currentRepoReference = $env:GITHUB_REPOSITORY $isFinalRepository = $false - + if ($repoSettings.templateUrl) { $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' $isFinalRepository = $templateRepoReference -ne $currentRepoReference } - + # Test that final repository is correctly detected $isFinalRepository | Should -Be $true - + # Apply customizations based on repository type if ($dstFileExists -and $type -eq 'workflow') { if (-not $isFinalRepository) { [Yaml]::ApplyCustomizations([ref] $srcContent, $finalRepoFile) } } - + # Verify that custom jobs were NOT applied (srcContent should not contain CustomJob-ShouldNotPersist) $srcContent | Should -Not -Match "CustomJob-ShouldNotPersist" $srcContent | Should -Not -Match "This custom job should not persist" - + # Verify that the base template structure is preserved $srcContent | Should -Match "Initialization:" $srcContent | Should -Match "Build:" @@ -135,7 +135,7 @@ Describe "Custom Job Removal Tests" { It 'Custom jobs should be applied from template repositories' { $testDir = Join-Path $PSScriptRoot "temp_custom_job_test" - + # Create a mock template CICD workflow (base workflow) $templateWorkflow = @( "name: 'CI/CD'", @@ -160,7 +160,7 @@ Describe "Custom Job Removal Tests" { " - name: PostProcess", " run: echo 'PostProcessing'" ) - + # Create a template repository workflow with custom jobs $templateRepoWorkflow = @( "name: 'CI/CD'", @@ -191,47 +191,47 @@ Describe "Custom Job Removal Tests" { " - name: PostProcess", " run: echo 'PostProcessing'" ) - + # Save test files $templateFile = Join-Path $testDir "template_cicd2.yaml" $templateRepoFile = Join-Path $testDir "template_repo_cicd.yaml" - + $templateWorkflow -join "`n" | Set-Content -Path $templateFile -Encoding UTF8 $templateRepoWorkflow -join "`n" | Set-Content -Path $templateRepoFile -Encoding UTF8 - + # Mock environment and repo settings for template repository (no templateUrl) $env:GITHUB_REPOSITORY = "testowner/template-repo" $repoSettings = @{} # No templateUrl - this is a template repository - + # Simulate the logic from CheckForUpdates.ps1 $srcContent = Get-Content -Path $templateFile -Raw $dstFileExists = Test-Path -Path $templateRepoFile $type = 'workflow' - + # Apply the final repository detection logic $currentRepoReference = $env:GITHUB_REPOSITORY $isFinalRepository = $false - + if ($repoSettings.templateUrl) { $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' $isFinalRepository = $templateRepoReference -ne $currentRepoReference } - + # Test that template repository is correctly detected $isFinalRepository | Should -Be $false - + # Apply customizations based on repository type if ($dstFileExists -and $type -eq 'workflow') { if (-not $isFinalRepository) { [Yaml]::ApplyCustomizations([ref] $srcContent, $templateRepoFile) } } - + # Verify that custom jobs WERE applied (srcContent should contain CustomJob-Template) $srcContent | Should -Match "CustomJob-Template" $srcContent | Should -Match "This is a template custom job" - + # Verify that the base template structure is preserved $srcContent | Should -Match "Initialization:" $srcContent | Should -Match "Build:" From d4878212569ddc027235c55e99edbd9b72e8a758 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:42:11 +0000 Subject: [PATCH 5/7] Add allowCustomJobsInEndRepos setting to control custom job behavior in final repositories Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 10 +- Scenarios/settings.md | 3 +- Tests/CheckForUpdates.Action.Test.ps1 | 56 +++++++++ Tests/CustomJobRemoval.Test.ps1 | 128 ++++++++++++++++++++ 4 files changed, 194 insertions(+), 3 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 829969576..3340d5891 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -256,8 +256,14 @@ foreach($checkfile in $checkfiles) { if ($type -eq 'workflow') { # Determine if current repository is a final repository (has templateUrl pointing to another repo) # Final repositories should not have custom jobs applied to prevent persistence of removed template jobs + # unless allowCustomJobsInEndRepos is explicitly set to true $currentRepoReference = $env:GITHUB_REPOSITORY $isFinalRepository = $false + $allowCustomJobsInEndRepos = $false + + if ($repoSettings.ContainsKey('allowCustomJobsInEndRepos')) { + $allowCustomJobsInEndRepos = $repoSettings.allowCustomJobsInEndRepos + } if ($repoSettings.templateUrl) { # Extract repository reference from templateUrl (e.g., "microsoft/AL-Go-PTE" from "https://github.com/microsoft/AL-Go-PTE@main") @@ -266,8 +272,8 @@ foreach($checkfile in $checkfiles) { $isFinalRepository = $templateRepoReference -ne $currentRepoReference } - if ($isFinalRepository) { - Write-Host "Skipping custom jobs from current repository (final repository using template: $($repoSettings.templateUrl)): $dstFile" + if ($isFinalRepository -and -not $allowCustomJobsInEndRepos) { + Write-Host "Skipping custom jobs from current repository (final repository using template: $($repoSettings.templateUrl), allowCustomJobsInEndRepos: $allowCustomJobsInEndRepos): $dstFile" } else { Write-Host "Apply customizations from current repository: $dstFile" diff --git a/Scenarios/settings.md b/Scenarios/settings.md index 27cc8f89b..1c1390d57 100644 --- a/Scenarios/settings.md +++ b/Scenarios/settings.md @@ -82,6 +82,7 @@ The repository settings are only read from the repository settings file (.github | buildModes | A list of build modes to use when building the AL-Go projects. Every AL-Go project will be built using each build mode. The following build modes have special meaning in AL-Go:
**Default**: Apps are compiled as they are in the source code.
**Clean**: Should be used for Clean Mode. Use [Conditional Settings](https://aka.ms/algosettings#conditional-settings) with buildMode set the 'Clean' to specify preprocessorSymbols for clean mode.
**Translated**: `TranslationFile` compiler feature is enabled when compiling the apps.

It is also possible to specify custom build modes by adding a build mode that is different than 'Default', 'Clean' or 'Translated' and use [conditional settings](https://aka.ms/algosettings#conditional-settings) to specify preprocessor symbols and other build settings for the build mode. | | useGitSubmodules | If your repository is using Git Submodules, you can set the `useGitSubmodules` setting to `"true"` or `"recursive"` in order to use these submodules during build workflows. If `useGitSubmodules` is not set, git submodules are not initialized. If the submodules reside in private repositories, you need to define a `gitSubmodulesToken` secret. Read [this](https://aka.ms/algosecrets#gitSubmodulesToken) for more information. | | commitOptions | If you want more control over how AL-Go creates pull requests or commits changes to the repository you can define `commitOptions`. It is a structure defining how you want AL-Go to handle automated commits or pull requests coming from AL-Go (e.g. for Update AL-Go System Files). The structure contains the following properties:
**messageSuffix** = A string you want to append to the end of commits/pull requests created by AL-Go. This can be useful if you are using the Azure Boards integration (or similar integration) to link commits to work items.
`createPullRequest` : A boolean defining whether AL-Go should create a pull request or attempt to push directly in the branch.
**pullRequestAutoMerge** = A boolean defining whether you want AL-Go pull requests to be set to auto-complete. This will auto-complete the pull requests once all checks are green and all required reviewers have approved.
**pullRequestLabels** = A list of labels to add to the pull request. The labels need to be created in the repository before they can be applied.
If you want different behavior in different AL-Go workflows you can add the `commitOptions` setting to your [workflow-specific settings files](https://github.com/microsoft/AL-Go/blob/main/Scenarios/settings.md#where-are-the-settings-located). | +| allowCustomJobsInEndRepos | Controls whether custom jobs are preserved in final repositories (repositories with a `templateUrl` pointing to another repository) when running Update AL-Go System Files. Default is **false**, which prevents custom jobs from persisting in final repositories to avoid jobs removed from templates continuing to exist. Set to **true** to allow custom jobs in final repositories. | false | | incrementalBuilds | A structure defining how you want AL-Go to handle incremental builds. When using incremental builds for a build, AL-Go will look for the latest successful CI/CD build, newer than the defined `retentionDays` and only rebuild projects or apps (based on `mode`) which needs to be rebuilt. The structure supports the following properties:
**onPush** = Determines whether incremental builds is enabled in CI/CD triggered by a merge/push event. Default is **false**.
**onPull_Request** = Determines whether incremental builds is enabled in Pull Requests. Default is **true**.
**onSchedule** = Determines whether incremental builds is enabled in CI/CD when running on a schedule. Default is **false**.
**retentionDays** = Number of days a successful build is good (and can be used for incremental builds). Default is **30**.
**mode** = Specifies the mode for incremental builds. Currently, two values are supported. Use **modifiedProjects** when you want to rebuild all apps in all modified projects and depending projects or **modifiedApps** if you want to rebuild modified apps and all apps with dependencies to this app.
**NOTE:** when running incremental builds, it is recommended to also set `workflowConcurrency` for the CI/CD workflow, as defined [here](https://aka.ms/algosettings#workflowConcurrency). | @@ -411,7 +412,7 @@ In the `needs` property, you specify which jobs should be complete before this j Custom jobs will be preserved when running Update AL-Go System Files, with the following behavior: - **Template repositories** (repositories without a `templateUrl` setting): Custom jobs are preserved and applied during updates -- **Final repositories** (repositories with a `templateUrl` pointing to another repository): Custom jobs from the current repository are NOT applied during updates to prevent persistence of jobs removed from the template +- **Final repositories** (repositories with a `templateUrl` pointing to another repository): Custom jobs from the current repository are NOT applied during updates to prevent persistence of jobs removed from the template, unless the `allowCustomJobsInEndRepos` setting is set to `true` **Note** that installing [apps from the GitHub marketplace](https://github.com/marketplace?type=apps) might require you to add custom jobs or steps to some of the workflows to get the right integration. In custom jobs, you can use any [actions from the GitHub marketplace](https://github.com/marketplace?type=actions). diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index f3a436621..25eb9e231 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -196,6 +196,62 @@ Describe "CheckForUpdates Action Tests" { $isFinalRepository | Should -Be $false -Because "Repository referencing itself should not be detected as final repository" } + + It 'Test allowCustomJobsInEndRepos setting behavior' { + # Test the allowCustomJobsInEndRepos setting logic used to determine if custom jobs should be applied + $env:GITHUB_REPOSITORY = "testowner/final-repo" + + # Test scenario 1: Final repository with allowCustomJobsInEndRepos = false (default) + $repoSettings = @{ + templateUrl = "https://github.com/testowner/template-repo@main" + } + + $currentRepoReference = $env:GITHUB_REPOSITORY + $isFinalRepository = $false + $allowCustomJobsInEndRepos = $false + + if ($repoSettings.ContainsKey('allowCustomJobsInEndRepos')) { + $allowCustomJobsInEndRepos = $repoSettings.allowCustomJobsInEndRepos + } + + if ($repoSettings.templateUrl) { + $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] + $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' + $isFinalRepository = $templateRepoReference -ne $currentRepoReference + } + + $shouldSkipCustomJobs = $isFinalRepository -and -not $allowCustomJobsInEndRepos + $shouldSkipCustomJobs | Should -Be $true -Because "Final repository with allowCustomJobsInEndRepos = false should skip custom jobs" + + # Test scenario 2: Final repository with allowCustomJobsInEndRepos = true + $repoSettings = @{ + templateUrl = "https://github.com/testowner/template-repo@main" + allowCustomJobsInEndRepos = $true + } + + $allowCustomJobsInEndRepos = $false + if ($repoSettings.ContainsKey('allowCustomJobsInEndRepos')) { + $allowCustomJobsInEndRepos = $repoSettings.allowCustomJobsInEndRepos + } + + $shouldSkipCustomJobs = $isFinalRepository -and -not $allowCustomJobsInEndRepos + $shouldSkipCustomJobs | Should -Be $false -Because "Final repository with allowCustomJobsInEndRepos = true should NOT skip custom jobs" + + # Test scenario 3: Template repository (should always apply custom jobs regardless of setting) + $repoSettings = @{ + allowCustomJobsInEndRepos = $false + } + + $isFinalRepository = $false + if ($repoSettings.templateUrl) { + $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] + $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' + $isFinalRepository = $templateRepoReference -ne $currentRepoReference + } + + $shouldSkipCustomJobs = $isFinalRepository -and -not $allowCustomJobsInEndRepos + $shouldSkipCustomJobs | Should -Be $false -Because "Template repository should always apply custom jobs regardless of allowCustomJobsInEndRepos setting" + } } Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" { diff --git a/Tests/CustomJobRemoval.Test.ps1 b/Tests/CustomJobRemoval.Test.ps1 index ca6c36bdc..ad4938ab8 100644 --- a/Tests/CustomJobRemoval.Test.ps1 +++ b/Tests/CustomJobRemoval.Test.ps1 @@ -237,4 +237,132 @@ Describe "Custom Job Removal Tests" { $srcContent | Should -Match "Build:" $srcContent | Should -Match "PostProcess:" } + + It 'Custom jobs should be applied in final repositories when allowCustomJobsInEndRepos is true' { + $testDir = Join-Path $PSScriptRoot "temp_custom_job_test" + + # Create a mock template CICD workflow (base workflow) + $templateWorkflow = @( + "name: 'CI/CD'", + "on:", + " workflow_dispatch:", + "jobs:", + " Initialization:", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Initialize", + " run: echo 'Initializing'", + " Build:", + " needs: [ Initialization ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Build", + " run: echo 'Building'", + " PostProcess:", + " needs: [ Initialization, Build ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: PostProcess", + " run: echo 'PostProcessing'" + ) + + # Create a final repository workflow with custom jobs + $finalRepoWorkflow = @( + "name: 'CI/CD'", + "on:", + " workflow_dispatch:", + "jobs:", + " Initialization:", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Initialize", + " run: echo 'Initializing'", + " CustomJob-ShouldPersist:", + " needs: [ Initialization ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Custom Step", + " run: echo 'This custom job should persist when allowCustomJobsInEndRepos is true'", + " Build:", + " needs: [ Initialization, CustomJob-ShouldPersist ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: Build", + " run: echo 'Building'", + " PostProcess:", + " needs: [ Initialization, Build ]", + " runs-on: [ windows-latest ]", + " steps:", + " - name: PostProcess", + " run: echo 'PostProcessing'" + ) + + # Save test files + $templateFile = Join-Path $testDir "template_cicd_allow.yaml" + $finalRepoFile = Join-Path $testDir "final_repo_cicd_allow.yaml" + + $templateWorkflow -join "`n" | Set-Content -Path $templateFile -Encoding UTF8 + $finalRepoWorkflow -join "`n" | Set-Content -Path $finalRepoFile -Encoding UTF8 + + # Mock environment and repo settings for final repository with allowCustomJobsInEndRepos = true + $env:GITHUB_REPOSITORY = "testowner/final-repo" + $repoSettings = @{ + templateUrl = "https://github.com/testowner/template-repo@main" + allowCustomJobsInEndRepos = $true + } + + # Test the workflow processing logic (simulating CheckForUpdates.ps1 behavior) + [Yaml]$templateYaml = [Yaml]::load($templateFile) + $srcContent = $templateYaml.content -join "`n" + + # Simulate the repository type detection and custom job handling + $currentRepoReference = $env:GITHUB_REPOSITORY + $isFinalRepository = $false + $allowCustomJobsInEndRepos = $false + + if ($repoSettings.ContainsKey('allowCustomJobsInEndRepos')) { + $allowCustomJobsInEndRepos = $repoSettings.allowCustomJobsInEndRepos + } + + if ($repoSettings.templateUrl) { + $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] + $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' + $isFinalRepository = $templateRepoReference -ne $currentRepoReference + } + + # Verify that it's detected as a final repository + $isFinalRepository | Should -Be $true + + # Verify that allowCustomJobsInEndRepos is set to true + $allowCustomJobsInEndRepos | Should -Be $true + + # When allowCustomJobsInEndRepos is true, custom jobs should be applied + # even in final repositories, so the condition should be: + # NOT ($isFinalRepository -and -not $allowCustomJobsInEndRepos) + $shouldApplyCustomJobs = -not ($isFinalRepository -and -not $allowCustomJobsInEndRepos) + $shouldApplyCustomJobs | Should -Be $true + + # Apply customizations if condition is met (simulating the actual behavior) + if ($shouldApplyCustomJobs) { + [Yaml]::ApplyCustomizations([ref] $srcContent, $finalRepoFile) + } + + # Verify that custom jobs are preserved in the final content + $srcContent | Should -Match "CustomJob-ShouldPersist" + } + + It 'allowCustomJobsInEndRepos setting defaults to false when not specified' { + $repoSettings = @{ + templateUrl = "https://github.com/testowner/template-repo@main" + # allowCustomJobsInEndRepos is not specified - should default to false + } + + $allowCustomJobsInEndRepos = $false + if ($repoSettings.ContainsKey('allowCustomJobsInEndRepos')) { + $allowCustomJobsInEndRepos = $repoSettings.allowCustomJobsInEndRepos + } + + # Should default to false + $allowCustomJobsInEndRepos | Should -Be $false + } } \ No newline at end of file From 990ed653e7a5d336458d4a4a18d6181f0e9f00bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 22 Jul 2025 21:57:20 +0000 Subject: [PATCH 6/7] Fix final repository detection logic to use standard AL-Go repositories Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 5 +- Tests/CustomJobRemoval.Test.ps1 | 67 +++++++++++++++++++-- 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 3340d5891..2911fac67 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -257,7 +257,6 @@ foreach($checkfile in $checkfiles) { # Determine if current repository is a final repository (has templateUrl pointing to another repo) # Final repositories should not have custom jobs applied to prevent persistence of removed template jobs # unless allowCustomJobsInEndRepos is explicitly set to true - $currentRepoReference = $env:GITHUB_REPOSITORY $isFinalRepository = $false $allowCustomJobsInEndRepos = $false @@ -269,7 +268,9 @@ foreach($checkfile in $checkfiles) { # Extract repository reference from templateUrl (e.g., "microsoft/AL-Go-PTE" from "https://github.com/microsoft/AL-Go-PTE@main") $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - $isFinalRepository = $templateRepoReference -ne $currentRepoReference + # Final repository is one where templateUrl doesn't point to standard AL-Go repositories + $standardAlGoRepos = @('microsoft/AL-Go-PTE', 'microsoft/AL-Go-AppSource', 'microsoft/AL-Go') + $isFinalRepository = $templateRepoReference -notin $standardAlGoRepos } if ($isFinalRepository -and -not $allowCustomJobsInEndRepos) { diff --git a/Tests/CustomJobRemoval.Test.ps1 b/Tests/CustomJobRemoval.Test.ps1 index ad4938ab8..7d24ffa92 100644 --- a/Tests/CustomJobRemoval.Test.ps1 +++ b/Tests/CustomJobRemoval.Test.ps1 @@ -104,13 +104,14 @@ Describe "Custom Job Removal Tests" { $type = 'workflow' # Apply the final repository detection logic - $currentRepoReference = $env:GITHUB_REPOSITORY $isFinalRepository = $false if ($repoSettings.templateUrl) { $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - $isFinalRepository = $templateRepoReference -ne $currentRepoReference + # Final repository is one where templateUrl doesn't point to standard AL-Go repositories + $standardAlGoRepos = @('microsoft/AL-Go-PTE', 'microsoft/AL-Go-AppSource', 'microsoft/AL-Go') + $isFinalRepository = $templateRepoReference -notin $standardAlGoRepos } # Test that final repository is correctly detected @@ -209,13 +210,14 @@ Describe "Custom Job Removal Tests" { $type = 'workflow' # Apply the final repository detection logic - $currentRepoReference = $env:GITHUB_REPOSITORY $isFinalRepository = $false if ($repoSettings.templateUrl) { $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - $isFinalRepository = $templateRepoReference -ne $currentRepoReference + # Final repository is one where templateUrl doesn't point to standard AL-Go repositories + $standardAlGoRepos = @('microsoft/AL-Go-PTE', 'microsoft/AL-Go-AppSource', 'microsoft/AL-Go') + $isFinalRepository = $templateRepoReference -notin $standardAlGoRepos } # Test that template repository is correctly detected @@ -316,7 +318,6 @@ Describe "Custom Job Removal Tests" { $srcContent = $templateYaml.content -join "`n" # Simulate the repository type detection and custom job handling - $currentRepoReference = $env:GITHUB_REPOSITORY $isFinalRepository = $false $allowCustomJobsInEndRepos = $false @@ -327,7 +328,9 @@ Describe "Custom Job Removal Tests" { if ($repoSettings.templateUrl) { $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - $isFinalRepository = $templateRepoReference -ne $currentRepoReference + # Final repository is one where templateUrl doesn't point to standard AL-Go repositories + $standardAlGoRepos = @('microsoft/AL-Go-PTE', 'microsoft/AL-Go-AppSource', 'microsoft/AL-Go') + $isFinalRepository = $templateRepoReference -notin $standardAlGoRepos } # Verify that it's detected as a final repository @@ -365,4 +368,56 @@ Describe "Custom Job Removal Tests" { # Should default to false $allowCustomJobsInEndRepos | Should -Be $false } + + It 'Repositories using standard AL-Go templates should NOT be considered final repositories' { + $standardTemplates = @( + "https://github.com/microsoft/AL-Go-PTE@main", + "https://github.com/microsoft/AL-Go-AppSource@main", + "https://github.com/microsoft/AL-Go@main" + ) + + foreach ($templateUrl in $standardTemplates) { + $repoSettings = @{ + templateUrl = $templateUrl + } + + $isFinalRepository = $false + if ($repoSettings.templateUrl) { + $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] + $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' + # Final repository is one where templateUrl doesn't point to standard AL-Go repositories + $standardAlGoRepos = @('microsoft/AL-Go-PTE', 'microsoft/AL-Go-AppSource', 'microsoft/AL-Go') + $isFinalRepository = $templateRepoReference -notin $standardAlGoRepos + } + + # Standard AL-Go templates should NOT be considered final repositories + $isFinalRepository | Should -Be $false -Because "Repository using $templateUrl should not be considered a final repository" + } + } + + It 'Repositories using custom templates should be considered final repositories' { + $customTemplates = @( + "https://github.com/myorg/my-custom-template@main", + "https://github.com/company/custom-algo-template@v1.0", + "https://github.com/team/modified-template@development" + ) + + foreach ($templateUrl in $customTemplates) { + $repoSettings = @{ + templateUrl = $templateUrl + } + + $isFinalRepository = $false + if ($repoSettings.templateUrl) { + $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] + $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' + # Final repository is one where templateUrl doesn't point to standard AL-Go repositories + $standardAlGoRepos = @('microsoft/AL-Go-PTE', 'microsoft/AL-Go-AppSource', 'microsoft/AL-Go') + $isFinalRepository = $templateRepoReference -notin $standardAlGoRepos + } + + # Custom templates should be considered final repositories + $isFinalRepository | Should -Be $true -Because "Repository using $templateUrl should be considered a final repository" + } + } } \ No newline at end of file From ed35ddc16f3ec069b11df0eec0608a78a93c8dd2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 22 Jul 2025 22:12:33 +0000 Subject: [PATCH 7/7] Fix repository type detection to use GitHub template repository API Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 27 ++- Tests/CheckForUpdates.Action.Test.ps1 | 139 ++++++++++----- Tests/CustomJobRemoval.Test.ps1 | 185 +++++++++++++------- 3 files changed, 234 insertions(+), 117 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 2911fac67..c1199754e 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -264,17 +264,28 @@ foreach($checkfile in $checkfiles) { $allowCustomJobsInEndRepos = $repoSettings.allowCustomJobsInEndRepos } - if ($repoSettings.templateUrl) { - # Extract repository reference from templateUrl (e.g., "microsoft/AL-Go-PTE" from "https://github.com/microsoft/AL-Go-PTE@main") - $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] - $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - # Final repository is one where templateUrl doesn't point to standard AL-Go repositories - $standardAlGoRepos = @('microsoft/AL-Go-PTE', 'microsoft/AL-Go-AppSource', 'microsoft/AL-Go') - $isFinalRepository = $templateRepoReference -notin $standardAlGoRepos + # Check if current repository is a GitHub template repository + # Template repositories should always allow custom jobs + # Final repositories (non-template) should respect the allowCustomJobsInEndRepos setting + try { + $headers = @{ + "Accept" = "application/vnd.github+json" + "Authorization" = "Bearer $token" + "X-GitHub-Api-Version" = "2022-11-28" + } + $repoInfo = (InvokeWebRequest -Headers $headers -Uri "$($env:GITHUB_API_URL)/repos/$($env:GITHUB_REPOSITORY)").Content | ConvertFrom-Json + $isTemplateRepository = $repoInfo.is_template + + # Final repository is one that is NOT marked as a template repository + $isFinalRepository = -not $isTemplateRepository + } + catch { + Write-Host "Warning: Could not determine if repository is a template. Assuming it's a final repository." + $isFinalRepository = $true } if ($isFinalRepository -and -not $allowCustomJobsInEndRepos) { - Write-Host "Skipping custom jobs from current repository (final repository using template: $($repoSettings.templateUrl), allowCustomJobsInEndRepos: $allowCustomJobsInEndRepos): $dstFile" + Write-Host "Skipping custom jobs from current repository (final repository, not marked as template, allowCustomJobsInEndRepos: $allowCustomJobsInEndRepos): $dstFile" } else { Write-Host "Apply customizations from current repository: $dstFile" diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 25eb9e231..0699865d4 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -144,69 +144,99 @@ Describe "CheckForUpdates Action Tests" { } } - It 'Test final repository detection logic' { - # Test the repository detection logic used to determine if custom jobs should be applied + It 'Test template repository detection logic' { + # Test the repository detection logic using GitHub API to determine if repository is a template $actionName = "CheckForUpdates" $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") + Import-Module (Join-Path $scriptRoot "..\Github-Helper.psm1") -DisableNameChecking -Force # Mock environment variables $env:GITHUB_REPOSITORY = "testowner/testrepo" + $env:GITHUB_API_URL = "https://api.github.com" - # Test scenario 1: Final repository using template - $repoSettings = @{ - templateUrl = "https://github.com/testowner/template-repo@main" - } + # Mock InvokeWebRequest for template repository + Mock InvokeWebRequest -MockWith { + return @{ + Content = '{"is_template": true}' + } + } -ParameterFilter { $uri -eq "https://api.github.com/repos/testowner/testrepo" } - # Extract repository reference logic (mirroring the actual code) - $currentRepoReference = $env:GITHUB_REPOSITORY + # Test scenario 1: Template repository (GitHub template) + $token = "mock-token" $isFinalRepository = $false - if ($repoSettings.templateUrl) { - $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] - $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - $isFinalRepository = $templateRepoReference -ne $currentRepoReference + try { + $headers = @{ + "Accept" = "application/vnd.github+json" + "Authorization" = "Bearer $token" + "X-GitHub-Api-Version" = "2022-11-28" + } + $repoInfo = (InvokeWebRequest -Headers $headers -Uri "$($env:GITHUB_API_URL)/repos/$($env:GITHUB_REPOSITORY)").Content | ConvertFrom-Json + $isTemplateRepository = $repoInfo.is_template + $isFinalRepository = -not $isTemplateRepository + } + catch { + $isFinalRepository = $true } - $isFinalRepository | Should -Be $true -Because "Repository using template should be detected as final repository" + $isFinalRepository | Should -Be $false -Because "GitHub template repository should not be detected as final repository" - # Test scenario 2: Template repository (no templateUrl) - $repoSettings = @{} + # Mock InvokeWebRequest for final repository + Mock InvokeWebRequest -MockWith { + return @{ + Content = '{"is_template": false}' + } + } -ParameterFilter { $uri -eq "https://api.github.com/repos/testowner/testrepo" } + # Test scenario 2: Final repository (not a GitHub template) $isFinalRepository = $false - if ($repoSettings.templateUrl) { - $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] - $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - $isFinalRepository = $templateRepoReference -ne $currentRepoReference + + try { + $repoInfo = (InvokeWebRequest -Headers $headers -Uri "$($env:GITHUB_API_URL)/repos/$($env:GITHUB_REPOSITORY)").Content | ConvertFrom-Json + $isTemplateRepository = $repoInfo.is_template + $isFinalRepository = -not $isTemplateRepository + } + catch { + $isFinalRepository = $true } - $isFinalRepository | Should -Be $false -Because "Repository without templateUrl should not be detected as final repository" + $isFinalRepository | Should -Be $true -Because "Non-template repository should be detected as final repository" - # Test scenario 3: Repository referencing itself (edge case) - $repoSettings = @{ - templateUrl = "https://github.com/testowner/testrepo@main" - } + # Test scenario 3: API error (fallback behavior) + Mock InvokeWebRequest -MockWith { + throw "API Error" + } -ParameterFilter { $uri -eq "https://api.github.com/repos/testowner/testrepo" } $isFinalRepository = $false - if ($repoSettings.templateUrl) { - $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] - $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - $isFinalRepository = $templateRepoReference -ne $currentRepoReference + + try { + $repoInfo = (InvokeWebRequest -Headers $headers -Uri "$($env:GITHUB_API_URL)/repos/$($env:GITHUB_REPOSITORY)").Content | ConvertFrom-Json + $isTemplateRepository = $repoInfo.is_template + $isFinalRepository = -not $isTemplateRepository + } + catch { + $isFinalRepository = $true } - $isFinalRepository | Should -Be $false -Because "Repository referencing itself should not be detected as final repository" + $isFinalRepository | Should -Be $true -Because "When API fails, repository should be assumed to be final repository" } It 'Test allowCustomJobsInEndRepos setting behavior' { - # Test the allowCustomJobsInEndRepos setting logic used to determine if custom jobs should be applied + # Test the allowCustomJobsInEndRepos setting logic for template vs final repositories $env:GITHUB_REPOSITORY = "testowner/final-repo" + $env:GITHUB_API_URL = "https://api.github.com" + Import-Module (Join-Path $PSScriptRoot "..\Actions\CheckForUpdates\..\Github-Helper.psm1") -DisableNameChecking -Force - # Test scenario 1: Final repository with allowCustomJobsInEndRepos = false (default) - $repoSettings = @{ - templateUrl = "https://github.com/testowner/template-repo@main" - } + # Test scenario 1: Final repository (not template) with allowCustomJobsInEndRepos = false (default) + Mock InvokeWebRequest -MockWith { + return @{ + Content = '{"is_template": false}' + } + } -ParameterFilter { $uri -eq "https://api.github.com/repos/testowner/final-repo" } - $currentRepoReference = $env:GITHUB_REPOSITORY + $repoSettings = @{} + $token = "mock-token" $isFinalRepository = $false $allowCustomJobsInEndRepos = $false @@ -214,10 +244,18 @@ Describe "CheckForUpdates Action Tests" { $allowCustomJobsInEndRepos = $repoSettings.allowCustomJobsInEndRepos } - if ($repoSettings.templateUrl) { - $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] - $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - $isFinalRepository = $templateRepoReference -ne $currentRepoReference + try { + $headers = @{ + "Accept" = "application/vnd.github+json" + "Authorization" = "Bearer $token" + "X-GitHub-Api-Version" = "2022-11-28" + } + $repoInfo = (InvokeWebRequest -Headers $headers -Uri "$($env:GITHUB_API_URL)/repos/$($env:GITHUB_REPOSITORY)").Content | ConvertFrom-Json + $isTemplateRepository = $repoInfo.is_template + $isFinalRepository = -not $isTemplateRepository + } + catch { + $isFinalRepository = $true } $shouldSkipCustomJobs = $isFinalRepository -and -not $allowCustomJobsInEndRepos @@ -225,7 +263,6 @@ Describe "CheckForUpdates Action Tests" { # Test scenario 2: Final repository with allowCustomJobsInEndRepos = true $repoSettings = @{ - templateUrl = "https://github.com/testowner/template-repo@main" allowCustomJobsInEndRepos = $true } @@ -238,15 +275,29 @@ Describe "CheckForUpdates Action Tests" { $shouldSkipCustomJobs | Should -Be $false -Because "Final repository with allowCustomJobsInEndRepos = true should NOT skip custom jobs" # Test scenario 3: Template repository (should always apply custom jobs regardless of setting) + Mock InvokeWebRequest -MockWith { + return @{ + Content = '{"is_template": true}' + } + } -ParameterFilter { $uri -eq "https://api.github.com/repos/testowner/final-repo" } + $repoSettings = @{ allowCustomJobsInEndRepos = $false } + $allowCustomJobsInEndRepos = $false + if ($repoSettings.ContainsKey('allowCustomJobsInEndRepos')) { + $allowCustomJobsInEndRepos = $repoSettings.allowCustomJobsInEndRepos + } + $isFinalRepository = $false - if ($repoSettings.templateUrl) { - $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] - $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - $isFinalRepository = $templateRepoReference -ne $currentRepoReference + try { + $repoInfo = (InvokeWebRequest -Headers $headers -Uri "$($env:GITHUB_API_URL)/repos/$($env:GITHUB_REPOSITORY)").Content | ConvertFrom-Json + $isTemplateRepository = $repoInfo.is_template + $isFinalRepository = -not $isTemplateRepository + } + catch { + $isFinalRepository = $true } $shouldSkipCustomJobs = $isFinalRepository -and -not $allowCustomJobsInEndRepos diff --git a/Tests/CustomJobRemoval.Test.ps1 b/Tests/CustomJobRemoval.Test.ps1 index 7d24ffa92..bb3aa874b 100644 --- a/Tests/CustomJobRemoval.Test.ps1 +++ b/Tests/CustomJobRemoval.Test.ps1 @@ -94,24 +94,37 @@ Describe "Custom Job Removal Tests" { # Mock environment and repo settings for final repository $env:GITHUB_REPOSITORY = "testowner/final-repo" - $repoSettings = @{ - templateUrl = "https://github.com/testowner/template-repo@main" - } + $env:GITHUB_API_URL = "https://api.github.com" + $repoSettings = @{} + + # Mock GitHub API for final repository (not a template) + Mock InvokeWebRequest -MockWith { + return @{ + Content = '{"is_template": false}' + } + } -ParameterFilter { $uri -eq "https://api.github.com/repos/testowner/final-repo" } # Simulate the logic from CheckForUpdates.ps1 $srcContent = Get-Content -Path $templateFile -Raw $dstFileExists = Test-Path -Path $finalRepoFile $type = 'workflow' - # Apply the final repository detection logic + # Apply the template repository detection logic (using GitHub API) + $token = "mock-token" $isFinalRepository = $false - if ($repoSettings.templateUrl) { - $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] - $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - # Final repository is one where templateUrl doesn't point to standard AL-Go repositories - $standardAlGoRepos = @('microsoft/AL-Go-PTE', 'microsoft/AL-Go-AppSource', 'microsoft/AL-Go') - $isFinalRepository = $templateRepoReference -notin $standardAlGoRepos + try { + $headers = @{ + "Accept" = "application/vnd.github+json" + "Authorization" = "Bearer $token" + "X-GitHub-Api-Version" = "2022-11-28" + } + $repoInfo = (InvokeWebRequest -Headers $headers -Uri "$($env:GITHUB_API_URL)/repos/$($env:GITHUB_REPOSITORY)").Content | ConvertFrom-Json + $isTemplateRepository = $repoInfo.is_template + $isFinalRepository = -not $isTemplateRepository + } + catch { + $isFinalRepository = $true } # Test that final repository is correctly detected @@ -200,24 +213,39 @@ Describe "Custom Job Removal Tests" { $templateWorkflow -join "`n" | Set-Content -Path $templateFile -Encoding UTF8 $templateRepoWorkflow -join "`n" | Set-Content -Path $templateRepoFile -Encoding UTF8 - # Mock environment and repo settings for template repository (no templateUrl) + # Mock environment and repo settings for template repository $env:GITHUB_REPOSITORY = "testowner/template-repo" - $repoSettings = @{} # No templateUrl - this is a template repository + $env:GITHUB_API_URL = "https://api.github.com" + $repoSettings = @{} + + # Mock GitHub API for template repository (marked as template) + Mock InvokeWebRequest -MockWith { + return @{ + Content = '{"is_template": true}' + } + } -ParameterFilter { $uri -eq "https://api.github.com/repos/testowner/template-repo" } # Simulate the logic from CheckForUpdates.ps1 $srcContent = Get-Content -Path $templateFile -Raw $dstFileExists = Test-Path -Path $templateRepoFile $type = 'workflow' - # Apply the final repository detection logic + # Apply the template repository detection logic (using GitHub API) + $token = "mock-token" $isFinalRepository = $false - if ($repoSettings.templateUrl) { - $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] - $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - # Final repository is one where templateUrl doesn't point to standard AL-Go repositories - $standardAlGoRepos = @('microsoft/AL-Go-PTE', 'microsoft/AL-Go-AppSource', 'microsoft/AL-Go') - $isFinalRepository = $templateRepoReference -notin $standardAlGoRepos + try { + $headers = @{ + "Accept" = "application/vnd.github+json" + "Authorization" = "Bearer $token" + "X-GitHub-Api-Version" = "2022-11-28" + } + $repoInfo = (InvokeWebRequest -Headers $headers -Uri "$($env:GITHUB_API_URL)/repos/$($env:GITHUB_REPOSITORY)").Content | ConvertFrom-Json + $isTemplateRepository = $repoInfo.is_template + $isFinalRepository = -not $isTemplateRepository + } + catch { + $isFinalRepository = $true } # Test that template repository is correctly detected @@ -308,16 +336,24 @@ Describe "Custom Job Removal Tests" { # Mock environment and repo settings for final repository with allowCustomJobsInEndRepos = true $env:GITHUB_REPOSITORY = "testowner/final-repo" + $env:GITHUB_API_URL = "https://api.github.com" $repoSettings = @{ - templateUrl = "https://github.com/testowner/template-repo@main" allowCustomJobsInEndRepos = $true } + # Mock GitHub API for final repository (not a template) + Mock InvokeWebRequest -MockWith { + return @{ + Content = '{"is_template": false}' + } + } -ParameterFilter { $uri -eq "https://api.github.com/repos/testowner/final-repo" } + # Test the workflow processing logic (simulating CheckForUpdates.ps1 behavior) [Yaml]$templateYaml = [Yaml]::load($templateFile) $srcContent = $templateYaml.content -join "`n" # Simulate the repository type detection and custom job handling + $token = "mock-token" $isFinalRepository = $false $allowCustomJobsInEndRepos = $false @@ -325,12 +361,18 @@ Describe "Custom Job Removal Tests" { $allowCustomJobsInEndRepos = $repoSettings.allowCustomJobsInEndRepos } - if ($repoSettings.templateUrl) { - $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] - $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - # Final repository is one where templateUrl doesn't point to standard AL-Go repositories - $standardAlGoRepos = @('microsoft/AL-Go-PTE', 'microsoft/AL-Go-AppSource', 'microsoft/AL-Go') - $isFinalRepository = $templateRepoReference -notin $standardAlGoRepos + try { + $headers = @{ + "Accept" = "application/vnd.github+json" + "Authorization" = "Bearer $token" + "X-GitHub-Api-Version" = "2022-11-28" + } + $repoInfo = (InvokeWebRequest -Headers $headers -Uri "$($env:GITHUB_API_URL)/repos/$($env:GITHUB_REPOSITORY)").Content | ConvertFrom-Json + $isTemplateRepository = $repoInfo.is_template + $isFinalRepository = -not $isTemplateRepository + } + catch { + $isFinalRepository = $true } # Verify that it's detected as a final repository @@ -356,7 +398,6 @@ Describe "Custom Job Removal Tests" { It 'allowCustomJobsInEndRepos setting defaults to false when not specified' { $repoSettings = @{ - templateUrl = "https://github.com/testowner/template-repo@main" # allowCustomJobsInEndRepos is not specified - should default to false } @@ -369,55 +410,69 @@ Describe "Custom Job Removal Tests" { $allowCustomJobsInEndRepos | Should -Be $false } - It 'Repositories using standard AL-Go templates should NOT be considered final repositories' { - $standardTemplates = @( - "https://github.com/microsoft/AL-Go-PTE@main", - "https://github.com/microsoft/AL-Go-AppSource@main", - "https://github.com/microsoft/AL-Go@main" - ) + It 'GitHub template repositories should always allow custom jobs' { + # Mock environment variables + $env:GITHUB_REPOSITORY = "testowner/template-repo" + $env:GITHUB_API_URL = "https://api.github.com" - foreach ($templateUrl in $standardTemplates) { - $repoSettings = @{ - templateUrl = $templateUrl + # Mock GitHub API for template repository + Mock InvokeWebRequest -MockWith { + return @{ + Content = '{"is_template": true}' } + } -ParameterFilter { $uri -eq "https://api.github.com/repos/testowner/template-repo" } - $isFinalRepository = $false - if ($repoSettings.templateUrl) { - $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] - $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - # Final repository is one where templateUrl doesn't point to standard AL-Go repositories - $standardAlGoRepos = @('microsoft/AL-Go-PTE', 'microsoft/AL-Go-AppSource', 'microsoft/AL-Go') - $isFinalRepository = $templateRepoReference -notin $standardAlGoRepos - } + $token = "mock-token" + $isFinalRepository = $false - # Standard AL-Go templates should NOT be considered final repositories - $isFinalRepository | Should -Be $false -Because "Repository using $templateUrl should not be considered a final repository" + try { + $headers = @{ + "Accept" = "application/vnd.github+json" + "Authorization" = "Bearer $token" + "X-GitHub-Api-Version" = "2022-11-28" + } + $repoInfo = (InvokeWebRequest -Headers $headers -Uri "$($env:GITHUB_API_URL)/repos/$($env:GITHUB_REPOSITORY)").Content | ConvertFrom-Json + $isTemplateRepository = $repoInfo.is_template + $isFinalRepository = -not $isTemplateRepository + } + catch { + $isFinalRepository = $true } + + # Template repositories should NOT be detected as final repositories + $isFinalRepository | Should -Be $false -Because "GitHub template repository should always allow custom jobs" } - It 'Repositories using custom templates should be considered final repositories' { - $customTemplates = @( - "https://github.com/myorg/my-custom-template@main", - "https://github.com/company/custom-algo-template@v1.0", - "https://github.com/team/modified-template@development" - ) + It 'Non-template repositories should be considered final repositories' { + # Mock environment variables + $env:GITHUB_REPOSITORY = "testowner/final-repo" + $env:GITHUB_API_URL = "https://api.github.com" - foreach ($templateUrl in $customTemplates) { - $repoSettings = @{ - templateUrl = $templateUrl + # Mock GitHub API for non-template repository + Mock InvokeWebRequest -MockWith { + return @{ + Content = '{"is_template": false}' } + } -ParameterFilter { $uri -eq "https://api.github.com/repos/testowner/final-repo" } - $isFinalRepository = $false - if ($repoSettings.templateUrl) { - $templateRepoUrl = $repoSettings.templateUrl.Split('@')[0] - $templateRepoReference = $templateRepoUrl.Split('/')[-2..-1] -join '/' - # Final repository is one where templateUrl doesn't point to standard AL-Go repositories - $standardAlGoRepos = @('microsoft/AL-Go-PTE', 'microsoft/AL-Go-AppSource', 'microsoft/AL-Go') - $isFinalRepository = $templateRepoReference -notin $standardAlGoRepos - } + $token = "mock-token" + $isFinalRepository = $false - # Custom templates should be considered final repositories - $isFinalRepository | Should -Be $true -Because "Repository using $templateUrl should be considered a final repository" + try { + $headers = @{ + "Accept" = "application/vnd.github+json" + "Authorization" = "Bearer $token" + "X-GitHub-Api-Version" = "2022-11-28" + } + $repoInfo = (InvokeWebRequest -Headers $headers -Uri "$($env:GITHUB_API_URL)/repos/$($env:GITHUB_REPOSITORY)").Content | ConvertFrom-Json + $isTemplateRepository = $repoInfo.is_template + $isFinalRepository = -not $isTemplateRepository + } + catch { + $isFinalRepository = $true } + + # Non-template repositories should be detected as final repositories + $isFinalRepository | Should -Be $true -Because "Non-template repository should be considered a final repository" } } \ No newline at end of file