Skip to content

Commit 7c4fec6

Browse files
Freddy Kristiansenfreddydkmazhelez
authored
Incremental Builds (#1304)
## Support incremental builds ### TODO - [x] Add release notes - [x] Add settings docs - [x] Add end 2 end tests - [x] Fix existing CI tests ### Use git diff instead of API In v6.1 and earlier versions, we use the GitHub API to determine last known good build and determine modified files in the Pull Request. It is however possible to have done modifications to the main branch after the last known good build - the changes will not be included in the PR and this the PR builds will only include the modified files in the PR together with the last known good build - potentially missing files. This version locates the SHA, which was used to build the last known good build and uses git diff to determine all changes from that build including the changes in the PR to build what's needed. git diff also supports many more files than the API, is faster and it doesn't count as API calls against the GitHub limit. ### Changed <workflow>Schedule to workflowSchedule Instead of using dynamic setting key name - use a workflow specific setting and deprecate the old keys. ### Add concurrency protection to workflows Add new workflow specific setting called - workflowConcurrency. It is used to control concurrency of workflows. Like with the `WorkflowSchedule` setting, this setting should be applied in workflow specific settings files or conditional settings. By default, all workflows allows for concurrency, except for the Create Release workflow. If you are using incremental builds in CI/CD it is also recommented to set WorkflowConcurrency to: ``` [ "group: ${{ github.workflow }}-${{ github.ref }}", "cancel-in-progress: true" ] ``` in order to cancel prior incremental builds on the same branch. Read more about workflow concurrency [here](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs). ### Remove warnings that tests doesn't exist if doNotBuildTests is true No reason to warn about missing test apps if you don't want to build them anyway ### Remove warnings and error when locating previous builds Getting rid of warnings like this: ![{224ABFB5-16B3-496B-B3F7-F33F474185F0}](https://github.com/user-attachments/assets/4dfe3748-3114-4f5d-b193-0fe14d42375e) due to DownloadProjectDependencies downloads artifacts double and errors like: ![{1C5434BC-A199-444C-AAD7-CA1362EA7D61}](https://github.com/user-attachments/assets/47b72ffc-751d-4432-b607-671a45fdd01c) if there wasn't a successful build in the repo. ### Check whether a package has been delivered to GitHubPackages or NuGet before delivering If the package already exists in the current version it has already been delivered (might be from an earlier build) --------- Co-authored-by: freddydk <[email protected]> Co-authored-by: Maria Zhelezova <[email protected]>
1 parent 053f5cc commit 7c4fec6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1438
-420
lines changed

.github/dependabot.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ updates:
66
directories:
77
- ".github/workflows/"
88
- "/Templates/**/.github/workflows/"
9+
- "/Actions/**/"
910
groups:
1011
External-Dependencies:
1112
applies-to: version-updates

.github/workflows/CleanupTempRepos.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ jobs:
7373
Import-Module (Join-Path "." "e2eTests/e2eTestHelper.psm1") -DisableNameChecking
7474
$owner = '${{ needs.Check.outputs.githubowner }}'
7575
$e2epat = '${{ Secrets.E2EPAT }}'
76-
SetTokenAndRepository -github -githubOwner $owner -e2epat $e2epat -repository ''
76+
SetTokenAndRepository -github -githubOwner $owner -token $e2epat -repository ''
7777
@(invoke-gh repo list $owner --limit 1000 -silent -returnValue) | ForEach-Object { $_.Split("`t")[0] } | Where-Object { "$_" -like "$owner/tmp*" } | ForEach-Object {
7878
$repo = $_
7979
Write-Host "https://github.com/$repo"

Actions/AL-Go-Helper.ps1

Lines changed: 90 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,23 @@ function Copy-HashTable() {
9898
}
9999
}
100100

101+
# Convert SecureString to plain text
102+
function Get-PlainText {
103+
Param(
104+
[parameter(ValueFromPipeline, Mandatory = $true)]
105+
[System.Security.SecureString] $SecureString
106+
)
107+
Process {
108+
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString);
109+
try {
110+
return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr);
111+
}
112+
finally {
113+
[Runtime.InteropServices.Marshal]::FreeBSTR($bstr);
114+
}
115+
}
116+
}
117+
101118
function IsPropertySecret {
102119
param (
103120
[string] $propertyName
@@ -407,7 +424,7 @@ function DownloadAndImportBcContainerHelper([string] $baseFolder = $ENV:GITHUB_W
407424
# Read Repository Settings file (without applying organization variables, repository variables or project settings files)
408425
# Override default BcContainerHelper version from AL-Go-Helper only if new version is specifically specified in repo settings file
409426
$repoSettings = Get-Content $repoSettingsPath -Encoding UTF8 | ConvertFrom-Json | ConvertTo-HashTable
410-
if ($repoSettings.Keys -contains "BcContainerHelperVersion") {
427+
if ($repoSettings.Keys -contains "BcContainerHelperVersion" -and $defaultBcContainerHelperVersion -notlike 'https://*') {
411428
$bcContainerHelperVersion = $repoSettings.BcContainerHelperVersion
412429
Write-Host "Using BcContainerHelper $bcContainerHelperVersion version"
413430
if ($bcContainerHelperVersion -like "https://*") {
@@ -528,7 +545,8 @@ function ReadSettings {
528545
[string] $userName = "$ENV:GITHUB_ACTOR",
529546
[string] $branchName = "$ENV:GITHUB_REF_NAME",
530547
[string] $orgSettingsVariableValue = "$ENV:ALGoOrgSettings",
531-
[string] $repoSettingsVariableValue = "$ENV:ALGoRepoSettings"
548+
[string] $repoSettingsVariableValue = "$ENV:ALGoRepoSettings",
549+
[switch] $silent
532550
)
533551

534552
# If the build is triggered by a pull request the refname will be the merge branch. To apply conditional settings we need to use the base branch
@@ -543,7 +561,7 @@ function ReadSettings {
543561

544562
if (Test-Path $path) {
545563
try {
546-
Write-Host "Applying settings from $path"
564+
if (!$silent.IsPresent) { Write-Host "Applying settings from $path" }
547565
$settings = Get-Content $path -Encoding UTF8 | ConvertFrom-Json
548566
if ($settings) {
549567
return $settings
@@ -554,7 +572,7 @@ function ReadSettings {
554572
}
555573
}
556574
else {
557-
Write-Host "No settings found in $path"
575+
if (!$silent.IsPresent) { Write-Host "No settings found in $path" }
558576
}
559577
return $null
560578
}
@@ -648,6 +666,13 @@ function ReadSettings {
648666
"cacheImageName" = "my"
649667
"cacheKeepDays" = 3
650668
"alwaysBuildAllProjects" = $false
669+
"incrementalBuilds" = [ordered]@{
670+
"onPush" = $false
671+
"onPull_Request" = $true
672+
"onSchedule" = $false
673+
"retentionDays" = 30
674+
"mode" = "modifiedApps" # modifiedProjects, modifiedApps
675+
}
651676
"microsoftTelemetryConnectionString" = "InstrumentationKey=cd2cc63e-0f37-4968-b99a-532411a314b8;IngestionEndpoint=https://northeurope-2.in.applicationinsights.azure.com/"
652677
"partnerTelemetryConnectionString" = ""
653678
"sendExtendedTelemetryToMicrosoft" = $false
@@ -733,32 +758,16 @@ function ReadSettings {
733758
if ("$conditionalSetting" -ne "") {
734759
$conditionMet = $true
735760
$conditions = @()
736-
if ($conditionalSetting.PSObject.Properties.Name -eq "buildModes") {
737-
$conditionMet = $conditionMet -and ($conditionalSetting.buildModes | Where-Object { $buildMode -like $_ })
738-
$conditions += @("buildMode: $buildMode")
739-
}
740-
if ($conditionalSetting.PSObject.Properties.Name -eq "branches") {
741-
$conditionMet = $conditionMet -and ($conditionalSetting.branches | Where-Object { $branchName -like $_ })
742-
$conditions += @("branchName: $branchName")
743-
}
744-
if ($conditionalSetting.PSObject.Properties.Name -eq "repositories") {
745-
$conditionMet = $conditionMet -and ($conditionalSetting.repositories | Where-Object { $repoName -like $_ })
746-
$conditions += @("repoName: $repoName")
747-
}
748-
if ($project -and $conditionalSetting.PSObject.Properties.Name -eq "projects") {
749-
$conditionMet = $conditionMet -and ($conditionalSetting.projects | Where-Object { $project -like $_ })
750-
$conditions += @("project: $project")
751-
}
752-
if ($workflowName -and $conditionalSetting.PSObject.Properties.Name -eq "workflows") {
753-
$conditionMet = $conditionMet -and ($conditionalSetting.workflows | Where-Object { $workflowName -like $_ })
754-
$conditions += @("workflowName: $workflowName")
755-
}
756-
if ($userName -and $conditionalSetting.PSObject.Properties.Name -eq "users") {
757-
$conditionMet = $conditionMet -and ($conditionalSetting.users | Where-Object { $userName -like $_ })
758-
$conditions += @("userName: $userName")
761+
@{"buildModes" = $buildMode; "branches" = $branchName; "repositories" = $repoName; "projects" = $project; "workflows" = $workflowName; "users" = $userName}.GetEnumerator() | ForEach-Object {
762+
$propName = $_.Key
763+
$propValue = $_.Value
764+
if ($conditionMet -and $conditionalSetting.PSObject.Properties.Name -eq $propName) {
765+
$conditionMet = $propValue -and $conditionMet -and ($conditionalSetting."$propName" | Where-Object { $propValue -like $_ })
766+
$conditions += @("$($propName): $propValue")
767+
}
759768
}
760769
if ($conditionMet) {
761-
Write-Host "Applying conditional settings for $($conditions -join ", ")"
770+
if (!$silent.IsPresent) { Write-Host "Applying conditional settings for $($conditions -join ", ")" }
762771
MergeCustomObjectIntoOrderedDictionary -dst $settings -src $conditionalSetting.settings
763772
}
764773
}
@@ -927,11 +936,6 @@ function AnalyzeRepo {
927936
)
928937

929938
$settings = $settings | Copy-HashTable
930-
931-
if (!$runningLocal) {
932-
Write-Host "::group::Analyzing repository"
933-
}
934-
935939
$projectPath = Join-Path $baseFolder $project -Resolve
936940

937941
# Check applicationDependency
@@ -1125,17 +1129,12 @@ function AnalyzeRepo {
11251129
if ($performanceToolkitApps.Contains($dep.id)) { $settings.installPerformanceToolkit = $true }
11261130
}
11271131
}
1128-
1129-
if (!$runningLocal) {
1130-
Write-Host "::endgroup::"
1131-
}
1132-
11331132
if (!$settings.doNotRunBcptTests -and -not $settings.bcptTestFolders) {
1134-
Write-Host "No performance test apps found in bcptTestFolders in $ALGoSettingsFile"
1133+
if (!$settings.doNotBuildTests) { Write-Host "No performance test apps found in bcptTestFolders in $ALGoSettingsFile" }
11351134
$settings.doNotRunBcptTests = $true
11361135
}
11371136
if (!$settings.doNotRunTests -and -not $settings.testFolders) {
1138-
if (!$doNotIssueWarnings) { OutputWarning -message "No test apps found in testFolders in $ALGoSettingsFile" }
1137+
if (-not ($doNotIssueWarnings -or $settings.doNotBuildTests)) { OutputWarning -message "No test apps found in testFolders in $ALGoSettingsFile" }
11391138
$settings.doNotRunTests = $true
11401139
}
11411140
if (-not $settings.appFolders) {
@@ -2120,7 +2119,7 @@ Function AnalyzeProjectDependencies {
21202119
Pop-Location
21212120
}
21222121

2123-
Write-Host "Folders containing apps are $($folders -join ',' )"
2122+
OutputMessageAndArray -Message "Folders containing apps" -arrayOfStrings $folders
21242123

21252124
$unknownDependencies = @()
21262125
$apps = @()
@@ -2383,6 +2382,7 @@ function RetryCommand {
23832382
try {
23842383
Invoke-Command $Command -ArgumentList $argumentList
23852384
if ($LASTEXITCODE -ne 0) {
2385+
$host.SetShouldExit(0);
23862386
throw "Command failed with exit code $LASTEXITCODE"
23872387
}
23882388
break
@@ -2438,6 +2438,22 @@ function GetProjectsFromRepository {
24382438
return @(GetMatchingProjects -projects $projects -selectProjects $selectProjects)
24392439
}
24402440

2441+
function GetFoldersFromAllProjects {
2442+
Param(
2443+
[string] $baseFolder
2444+
)
2445+
2446+
$settings = ReadSettings -baseFolder $baseFolder
2447+
$projects = GetProjectsFromRepository -baseFolder $baseFolder -projectsFromSettings $settings.projects
2448+
$folders = @()
2449+
foreach($project in $projects) {
2450+
$projectSettings = ReadSettings -project $project -baseFolder $baseFolder -silent
2451+
ResolveProjectFolders -baseFolder $baseFolder -project $project -projectSettings ([ref] $projectSettings)
2452+
$folders += @( @($projectSettings.appFolders) + @($projectSettings.testFolders) + @($projectSettings.bcptTestFolders) | ForEach-Object { Join-Path $project "$_".Substring(2) } )
2453+
}
2454+
return $folders
2455+
}
2456+
24412457
function GetPackageVersion($packageName) {
24422458
$alGoPackages = Get-Content -Path "$PSScriptRoot\Packages.json" | ConvertFrom-Json
24432459

@@ -2521,3 +2537,36 @@ function ConnectAz {
25212537
throw "Error trying to authenticate to Azure. Error was $($_.Exception.Message)"
25222538
}
25232539
}
2540+
2541+
function OutputMessageAndArray {
2542+
Param(
2543+
[string] $message,
2544+
[string[]] $arrayOfStrings
2545+
)
2546+
Write-Host "$($message):"
2547+
if (!$arrayOfStrings) {
2548+
Write-Host "- None"
2549+
}
2550+
else {
2551+
$arrayOfStrings | ForEach-Object {
2552+
Write-Host "- $_"
2553+
}
2554+
}
2555+
}
2556+
2557+
<#
2558+
.SYNOPSIS
2559+
Run an executable and check the exit code
2560+
.EXAMPLE
2561+
RunAndCheck git checkout -b xxx
2562+
#>
2563+
function RunAndCheck {
2564+
$ErrorActionPreference = 'SilentlyContinue'
2565+
$rest = if ($args.Count -gt 1) { $args[1..($args.Count - 1)] } else { $null }
2566+
& $args[0] $rest
2567+
$ErrorActionPreference = 'STOP'
2568+
if ($LASTEXITCODE -ne 0) {
2569+
$host.SetShouldExit(0)
2570+
throw "$($args[0]) $($rest | ForEach-Object { $_ }) failed with exit code $LASTEXITCODE"
2571+
}
2572+
}

Actions/AL-Go-TestRepoHelper.ps1

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@ function Test-Shell {
4343
}
4444
}
4545

46+
function Test-Deprecations {
47+
Param(
48+
[HashTable] $json,
49+
[string] $settingsDescription
50+
)
51+
52+
# cleanModePreprocessorSymbols is deprecated
53+
if ($json.Keys -contains 'cleanModePreprocessorSymbols') {
54+
OutputWarning -Message "cleanModePreprocessorSymbols in $settingsDescription is deprecated. See https://aka.ms/algodeprecations#cleanModePreprocessorSymbols"
55+
}
56+
57+
# <workflowName>Schedule is deprecated
58+
($json.Keys | Where-Object {$_ -like '*Schedule' -and $_ -ne 'WorkflowSchedule'}) | ForEach-Object {
59+
OutputWarning -Message "$_ in $settingsDescription is deprecated. See https://aka.ms/algodeprecations#_workflow_Schedule. This warning will become an error in the future."
60+
}
61+
}
62+
4663
function Test-SettingsJson {
4764
Param(
4865
[hashtable] $json,
@@ -51,6 +68,8 @@ function Test-SettingsJson {
5168
[string] $type
5269
)
5370

71+
Test-Deprecations -json $json -settingsDescription $settingsDescription
72+
5473
Test-Shell -json $json -settingsDescription $settingsDescription -property 'shell'
5574
Test-Shell -json $json -settingsDescription $settingsDescription -property 'gitHubRunnerShell'
5675

@@ -71,18 +90,17 @@ function Test-SettingsJson {
7190
if ($type -eq 'Workflow') {
7291
# Test for things that should / should not exist in a workflow settings file
7392
}
93+
else {
94+
# workflowSchedule and workflowConcurrency should only exist in workflow specific settings files (or conditional settings - not tested)
95+
Test-Property -settingsDescription $settingsDescription -json $json -key 'workflowSchedule' -maynot
96+
Test-Property -settingsDescription $settingsDescription -json $json -key 'workflowConcurrency' -maynot
97+
}
7498
if ($type -eq 'Variable') {
7599
# Test for things that should / should not exist in a settings variable
76100
}
77101
if ($type -eq 'Project' -or $type -eq 'Workflow') {
78102
# templateUrl should not be in Project or Workflow settings
79103
Test-Property -settingsDescription $settingsDescription -json $json -key 'templateUrl' -maynot
80-
81-
# schedules and runs-on should not be in Project or Workflow settings
82-
# These properties are used in Update AL-Go System Files, hence they should only be in Repo settings
83-
'nextMajorSchedule','nextMinorSchedule','currentSchedule','runs-on' | ForEach-Object {
84-
Test-Property -settingsDescription $settingsDescription -json $json -key $_ -shouldnot
85-
}
86104
}
87105
}
88106

@@ -100,7 +118,7 @@ function Test-JsonStr {
100118

101119
try {
102120
$json = $jsonStr | ConvertFrom-Json | ConvertTo-HashTable
103-
Test-SettingsJson -json $json -settingsDescription $settingsDescription -type:$type
121+
Test-SettingsJson -json $json -settingsDescription $settingsDescription -type $type
104122
}
105123
catch {
106124
OutputError "$($_.Exception.Message.Replace("`r",'').Replace("`n",' ')) in $settingsDescription"

Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -303,14 +303,53 @@ function GetWorkflowContentWithChangesFromSettings {
303303

304304
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($srcFile)
305305
$yaml = [Yaml]::Load($srcFile)
306-
$workflowScheduleKey = "$($baseName)Schedule"
306+
$yamlName = $yaml.get('name:')
307+
if ($yamlName) {
308+
$workflowName = $yamlName.content.SubString('name:'.Length).Trim().Trim('''"').Trim()
309+
}
310+
else {
311+
$workflowName = $baseName
312+
}
313+
314+
$workflowScheduleKey = "WorkflowSchedule"
315+
$workflowConcurrencyKey = "WorkflowConcurrency"
316+
foreach($key in @($workflowScheduleKey,$workflowConcurrencyKey)) {
317+
if ($repoSettings.Keys -contains $key -and ($repoSettings."$key")) {
318+
throw "The $key setting is not allowed in the global repository settings. Please use the workflow specific settings file or conditional settings."
319+
}
320+
}
307321

308-
# Any workflow (except for the PullRequestHandler and reusable workflows (_*)) can have a RepoSetting called <workflowname>Schedule, which will be used to set the schedule for the workflow
322+
# Re-read settings and this time include workflow specific settings
323+
$repoSettings = ReadSettings -buildMode '' -project '' -workflowName $workflowName -userName '' -branchName '' | ConvertTo-HashTable -recurse
324+
325+
# Old Schedule key is deprecated, but still supported
326+
$oldWorkflowScheduleKey = "$($baseName)Schedule"
327+
if ($repoSettings.Keys -contains $oldWorkflowScheduleKey) {
328+
# DEPRECATION: REPLACE WITH ERROR AFTER October 1st 2025 --->
329+
if ($repoSettings.Keys -contains $workflowScheduleKey) {
330+
OutputWarning "Both $oldWorkflowScheduleKey and $workflowScheduleKey are defined in the settings file. $oldWorkflowScheduleKey will be ignored. This warning will become an error in the future"
331+
}
332+
else {
333+
Trace-DeprecationWarning -Message "$oldWorkflowScheduleKey is deprecated" -DeprecationTag "_workflow_Schedule" -WillBecomeError
334+
# Convert the old <workflow>Schedule setting to the new WorkflowSchedule setting
335+
$repoSettings."$workflowScheduleKey" = @{ "cron" = $repoSettings."$oldWorkflowScheduleKey" }
336+
}
337+
# <--- REPLACE WITH ERROR AFTER October 1st 2025
338+
}
339+
340+
# Any workflow (except for the PullRequestHandler and reusable workflows (_*)) can have concurrency and schedule defined
309341
if ($baseName -ne "PullRequestHandler" -and $baseName -notlike '_*') {
342+
# Add Schedule and Concurrency settings to the workflow
310343
if ($repoSettings.Keys -contains $workflowScheduleKey) {
311-
# Read the section under the on: key and add the schedule section
312-
$yamlOn = $yaml.Get('on:/')
313-
$yaml.Replace('on:/', $yamlOn.content+@('schedule:', " - cron: '$($repoSettings."$workflowScheduleKey")'"))
344+
if ($repoSettings."$workflowScheduleKey" -isnot [hashtable] -or $repoSettings."$workflowScheduleKey".Keys -notcontains 'cron' -or $repoSettings."$workflowScheduleKey".cron -isnot [string]) {
345+
throw "The $workflowScheduleKey setting must be a structure containing a cron property"
346+
}
347+
# Replace or add the schedule part under the on: key
348+
$yaml.ReplaceOrAdd('on:/', 'schedule:', @("- cron: '$($repoSettings."$workflowScheduleKey".cron)'"))
349+
}
350+
if ($repoSettings.Keys -contains $workflowConcurrencyKey) {
351+
# Replace or add the concurrency part
352+
$yaml.ReplaceOrAdd('', 'concurrency:', $repoSettings."$workflowConcurrencyKey")
314353
}
315354
}
316355

0 commit comments

Comments
 (0)