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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Actions/.Modules/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,8 @@
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable Copilot code review workflows."
"default": false,
"description": "Enable Copilot code review in PTE and AppSource repositories. Update AL-Go System Files adds the automatic review triggers only when true; otherwise it removes the review workflows. Raw templates are manual-only and skip manual runs."
},
"branches": {
"type": "array",
Expand Down
26 changes: 23 additions & 3 deletions Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ function ModifyPullRequestHandlerWorkflow {
$yaml.Replace("on:/$($repoSettings.pullRequestTrigger):/branches:", "branches: [ '$($CICDPullRequestBranches -join "', '")' ]")
}

<#
.SYNOPSIS
Adds the pull request trigger and configured target branches to the enabled review intake workflow.
#>
function ModifyCopilotPRReviewWorkflow {
Param(
[Yaml] $yaml,
Expand All @@ -150,7 +154,10 @@ function ModifyCopilotPRReviewWorkflow {
}

$branchesJson = ConvertTo-Json -InputObject $codeReviewBranches -Compress
$yaml.Replace("on:/pull_request:/branches:", "branches: $branchesJson")
$yaml.ReplaceOrAdd('on:/', 'pull_request:', @(
"branches: $branchesJson"
'types: [ opened, reopened, synchronize, ready_for_review ]'
))
}

function ModifyRunsOnAndShell {
Expand Down Expand Up @@ -593,8 +600,21 @@ function GetWorkflowContentWithChangesFromSettings {
ModifyPullRequestHandlerWorkflow -yaml $yaml -repoSettings $repoSettings
}

if ([System.IO.Path]::GetFileName($srcFile) -eq "CopilotPRReview.yaml") {
ModifyCopilotPRReviewWorkflow -yaml $yaml -repoSettings $repoSettings
if (
$repoSettings.Keys -contains 'CodeReviewAgent' -and
$repoSettings.CodeReviewAgent -is [System.Collections.IDictionary] -and
$repoSettings.CodeReviewAgent.Keys -contains 'enabled' -and
$repoSettings.CodeReviewAgent.enabled
) {
if ($baseName -eq 'CopilotPRReview') {
ModifyCopilotPRReviewWorkflow -yaml $yaml -repoSettings $repoSettings
}
elseif ($baseName -eq 'CopilotPRReviewRunner') {
$yaml.ReplaceOrAdd('on:/', 'workflow_run:', @(
'workflows: [ Copilot PR Review ]'
'types: [ completed ]'
))
}
}

$criticalWorkflows = @('UpdateGitHubGoSystemFiles', 'Troubleshooting', 'CopilotPRReviewRunner')
Expand Down
6 changes: 5 additions & 1 deletion Scenarios/Agents/CopilotCodeReview.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ Add the following repository-level setting to `.github/AL-Go-Settings.json`:
}
```

Run the **Update AL-Go System Files** workflow to add the review workflows. Subsequent eligible pull requests targeting a branch configured in `CodeReviewAgent.branches` will be reviewed automatically.
Code review is off by default. The PTE and AppSource templates include review workflow files, but these files do not automatically review pull requests. Running them manually also skips all review jobs.

To turn on automatic reviews, set `CodeReviewAgent.enabled` to `true` and run **Update AL-Go System Files** using the latest AL-Go version. This configures the workflows to review eligible pull requests targeting the branches in `CodeReviewAgent.branches`.

To turn reviews off, set `CodeReviewAgent.enabled` to `false` and run **Update AL-Go System Files** again. This removes both review workflows.

BCQuality defaults to the trusted `microsoft/BCQuality` marketplace release, retains all content layers, and supplies the internal AL review context. Advanced filtering remains available through `BCQuality.enabledLayers`, `disabledSkills`, and `knowledge`. An empty `enabledLayers` list retains Microsoft, community, and custom content; an explicit list retains only those layers. `CodeReviewAgent.branches` controls the target branches reviewed independently of pull request build branches. Its empty default means `main`; an explicit list replaces that fallback and uses ordered GitHub Actions branch patterns, including `!` exclusions. The trusted runner checks the authoritative pull request base branch against these patterns before review. `model` controls final self-review and consolidation, while `leafModel` controls the isolated review processes. `excludedAuthors` contains GitHub login names whose pull requests should not be reviewed. The author list is empty by default.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
name: Copilot PR Review

on:
pull_request:
branches: [ 'main' ]
types: [ opened, reopened, synchronize, ready_for_review ]
workflow_dispatch:

concurrency:
group: copilot-pr-review-${{ github.event.pull_request.number }}
Expand All @@ -18,6 +16,7 @@ permissions:

jobs:
Intake:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Upload pull request metadata
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
name: Copilot PR Review Runner

on:
workflow_run:
workflows: [ Copilot PR Review ]
types: [ completed ]
workflow_dispatch:

concurrency:
group: copilot-pr-review-runner-${{ github.event.workflow_run.id }}
Expand All @@ -21,7 +19,7 @@ env:

jobs:
prepare:
if: github.event.workflow_run.conclusion == 'success'
if: github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: [ windows-latest ]
permissions:
actions: read
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
name: Copilot PR Review

on:
pull_request:
branches: [ 'main' ]
types: [ opened, reopened, synchronize, ready_for_review ]
workflow_dispatch:

concurrency:
group: copilot-pr-review-${{ github.event.pull_request.number }}
Expand All @@ -18,6 +16,7 @@ permissions:

jobs:
Intake:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Upload pull request metadata
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
name: Copilot PR Review Runner

on:
workflow_run:
workflows: [ Copilot PR Review ]
types: [ completed ]
workflow_dispatch:

concurrency:
group: copilot-pr-review-runner-${{ github.event.workflow_run.id }}
Expand All @@ -21,7 +19,7 @@ env:

jobs:
prepare:
if: github.event.workflow_run.conclusion == 'success'
if: github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: [ windows-latest ]
permissions:
actions: read
Expand Down
102 changes: 102 additions & 0 deletions Tests/CheckForUpdates.Action.Test.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,74 @@ Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" {
{ ModifyRunsOnAndShell -yaml $yaml -repoSettings $repoSettings } | Should -Throw '*The shell cannot be set to powershell when runs-on is ubuntu-latest*'
}

It 'Adds Copilot review triggers only when enabled in <Template>' -TestCases @(
@{ Template = 'Per Tenant Extension' }
@{ Template = 'AppSource App' }
) {
param($Template)
. (Join-Path $scriptRoot '..\AL-Go-Helper.ps1' -Resolve)
$workflowFolder = Join-Path $PSScriptRoot "..\Templates\$Template\.github\workflows"
$reviewSettings = @{ enabled = $true }
Mock ReadSettings { @{ 'runs-on' = 'windows-latest'; shell = 'pwsh'; CodeReviewAgent = $reviewSettings } }

foreach ($branches in @(
@{ expected = @('main') }
@{ configured = @(); expected = @('main') }
@{ configured = @('main', 'release/**', '!release/**-alpha'); expected = @('main', 'release/**', '!release/**-alpha') }
@{ configured = @("release/o'clock", 'release/"quoted"'); expected = @("release/o'clock", 'release/"quoted"') }
)) {
if ($branches.ContainsKey('configured')) {
$reviewSettings.branches = $branches.configured
}
$intakeContent = GetWorkflowContentWithChangesFromSettings -srcFile (Join-Path $workflowFolder 'CopilotPRReview.yaml') -repoSettings @{} -depth 1
$intake = [Yaml]::new($intakeContent.Split("`n"))
$intake.GetNextLevel('on:/') | Should -Be @('workflow_dispatch:', 'pull_request:')
$intake.GetProperty('on:/pull_request:/branches:') | Should -Be (ConvertTo-Json -InputObject $branches.expected -Compress)
$intake.GetPropertyArray('on:/pull_request:/types:') | Should -Be @('opened', 'reopened', 'synchronize', 'ready_for_review')
}

$runnerContent = GetWorkflowContentWithChangesFromSettings -srcFile (Join-Path $workflowFolder 'CopilotPRReviewRunner.yaml') -repoSettings @{} -depth 1
$runner = [Yaml]::new($runnerContent.Split("`n"))
$runner.GetNextLevel('on:/') | Should -Be @('workflow_dispatch:', 'workflow_run:')
$runner.GetPropertyArray('on:/workflow_run:/workflows:') | Should -Be @('Copilot PR Review')
$runner.GetPropertyArray('on:/workflow_run:/types:') | Should -Be @('completed')

foreach ($workflow in @(
@{ yaml = $intake; file = 'CopilotPRReview.yaml' }
@{ yaml = $runner; file = 'CopilotPRReviewRunner.yaml' }
)) {
$original = [Yaml]::Load((Join-Path $workflowFolder $workflow.file))
$original.Replace('on:', @())
$workflow.yaml.Replace('on:', @())
$workflow.yaml.content | Should -Be $original.content -Because 'Opt-in must preserve job guards, permissions, concurrency and steps'
}
}

It 'Keeps Copilot review triggers inert with <State> settings in <Template>' -TestCases @(
@{ Template = 'Per Tenant Extension'; State = 'missing'; ReviewSettings = $null }
@{ Template = 'Per Tenant Extension'; State = 'disabled'; ReviewSettings = @{ enabled = $false } }
@{ Template = 'Per Tenant Extension'; State = 'missing enabled'; ReviewSettings = @{ branches = @('release/**') } }
@{ Template = 'AppSource App'; State = 'missing'; ReviewSettings = $null }
@{ Template = 'AppSource App'; State = 'disabled'; ReviewSettings = @{ enabled = $false } }
@{ Template = 'AppSource App'; State = 'missing enabled'; ReviewSettings = @{ branches = @('release/**') } }
) {
param($Template, $State, $ReviewSettings)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
. (Join-Path $scriptRoot '..\AL-Go-Helper.ps1' -Resolve)
$settings = @{ 'runs-on' = 'windows-latest'; shell = 'pwsh' }
if ($null -ne $ReviewSettings) {
$settings.CodeReviewAgent = $ReviewSettings
}
Mock ReadSettings { $settings }

foreach ($file in @('CopilotPRReview.yaml', 'CopilotPRReviewRunner.yaml')) {
$source = Join-Path $PSScriptRoot "..\Templates\$Template\.github\workflows\$file"
$modified = GetWorkflowContentWithChangesFromSettings -srcFile $source -repoSettings @{} -depth 1
$yaml = [Yaml]::new($modified.Split("`n"))
$yaml.GetNextLevel('on:/') | Should -Be @('workflow_dispatch:') -Because "$State settings must not add automatic review triggers"
$yaml.content | Should -Be ([Yaml]::Load($source)).content
}
}

It 'ModifyRunsOnAndShell rejects unsupported shells' {
$yaml = [Yaml]::new(@(
"jobs:",
Expand Down Expand Up @@ -2509,6 +2577,40 @@ Describe "GetFilesToUpdate (real template)" {
$realAppSourceAppTemplateFolder = Join-Path $PSScriptRoot "../Templates/AppSource App" -Resolve
}

It 'Includes or excludes both review workflows for <State> in <Template>' -TestCases @(
@{ Template = 'Per Tenant Extension'; State = 'missing'; Enabled = $null }
@{ Template = 'Per Tenant Extension'; State = 'disabled'; Enabled = $false }
@{ Template = 'Per Tenant Extension'; State = 'enabled'; Enabled = $true }
@{ Template = 'AppSource App'; State = 'missing'; Enabled = $null }
@{ Template = 'AppSource App'; State = 'disabled'; Enabled = $false }
@{ Template = 'AppSource App'; State = 'enabled'; Enabled = $true }
) {
param($Template, $State, $Enabled)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
$settings = @{
type = $(if ($Template -eq 'Per Tenant Extension') { 'PTE' } else { 'AppSource App' })
powerPlatformSolutionFolder = ''
unusedALGoSystemFiles = @()
customALGoFiles = @{ filesToInclude = @(); filesToExclude = @() }
}
if ($null -ne $Enabled) {
$settings.CodeReviewAgent = @{ enabled = $Enabled }
}
$templateFolder = Join-Path $PSScriptRoot "..\Templates\$Template" -Resolve
$filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder

foreach ($file in @('CopilotPRReview.yaml', 'CopilotPRReviewRunner.yaml')) {
$source = Join-Path $templateFolder ".github/workflows/$file"
if ($Enabled) {
$filesToInclude.sourceFullPath | Should -Contain $source -Because "$State review workflows must be included"
@($filesToExclude | ForEach-Object { $_.sourceFullPath }) | Should -Not -Contain $source
}
else {
$filesToInclude.sourceFullPath | Should -Not -Contain $source -Because "$State review settings must exclude the workflows"
$filesToExclude.sourceFullPath | Should -Contain $source
}
}
}

It 'Return the correct files to exclude when type is PTE and powerPlatformSolutionFolder is not empty' {
$settings = @{
type = "PTE"
Expand Down
24 changes: 24 additions & 0 deletions Tests/WorkflowSanitation/WorkflowFileContent.Test.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,30 @@ Describe "Copilot code review workflows" {
$reviewAction = Get-Content -Path (Join-Path $PSScriptRoot '..\..\Actions\ReviewAgent\ReviewAgent.ps1' -Resolve) -Encoding UTF8 -Raw
}

It 'Ships manual-only review templates whose entire job graph skips manual dispatch in <Template>' -TestCases @(
@{ Template = 'Per Tenant Extension' }
@{ Template = 'AppSource App' }
) {
param($Template)
. (Join-Path $PSScriptRoot '..\..\Actions\CheckForUpdates\yamlclass.ps1')
$folder = Join-Path $PSScriptRoot "..\..\Templates\$Template\.github\workflows"
$intake = [Yaml]::Load((Join-Path $folder 'CopilotPRReview.yaml'))
$runner = [Yaml]::Load((Join-Path $folder 'CopilotPRReviewRunner.yaml'))

$intake.GetNextLevel('on:/') | Should -Be @('workflow_dispatch:')
$runner.GetNextLevel('on:/') | Should -Be @('workflow_dispatch:')
$intake.Get('on:/workflow_dispatch:/').content | Should -BeNullOrEmpty
$runner.Get('on:/workflow_dispatch:/').content | Should -BeNullOrEmpty
$intake.GetNextLevel('jobs:/') | Should -Be @('Intake:')
$intake.GetProperty('jobs:/Intake:/if:') | Should -Be "github.event_name == 'pull_request'"
$runner.GetNextLevel('jobs:/') | Should -Be @('prepare:', 'review:', 'publish:')
$runner.GetProperty('jobs:/prepare:/if:') | Should -Be "github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'" -Because 'A successful manual intake run must not start runner preparation without PR metadata'
$runner.GetProperty('jobs:/review:/needs:') | Should -Be 'prepare'
$runner.GetProperty('jobs:/review:/if:') | Should -Be "needs.prepare.outputs.eligible == 'true'"
$runner.GetPropertyArray('jobs:/publish:/needs:') | Should -Be @('prepare', 'review')
$runner.GetProperty('jobs:/publish:/if:') | Should -Be "needs.review.result == 'success'"
}

It 'Reads the repository-level opt-in setting from trusted content' {
$runnerWorkflow | Should -Match 'name:\s+Checkout trusted settings'
$runnerWorkflow | Should -Match 'get:\s+CodeReviewAgent,BCQuality'
Expand Down
Loading