Skip to content
Open
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
169 changes: 25 additions & 144 deletions .github/workflows/BuildMissingImages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,66 +31,15 @@ jobs:
RUNNUMBEROFFSET: ${{ vars.RUNNUMBEROFFSET }}
PushToProd: ${{ github.event.inputs.PushToProd }}
run: |
$erroractionpreference = "STOP"
try {
$bctags = @('ltsc2016','ltsc2019','ltsc2022','ltsc2025')
$servercoretags = @{
"ltsc2016" = "4.8-windowsservercore-ltsc2016"
"ltsc2019" = "4.8-windowsservercore-ltsc2019"
"ltsc2022" = "4.8.1-windowsservercore-ltsc2022"
"ltsc2025" = "4.8.1-windowsservercore-ltsc2025"
}
$pushToProd = $true
if ($env:GITHUB_EVENT_NAME -eq "workflow_dispatch") {
$pushToProd = $true
if ($env:GITHUB_EVENT_NAME -eq "workflow_dispatch") {
$pushToProd = $env:PushToProd -eq 'True'
}
$tags = @($bctags | ForEach-Object { "$_-dev"; "$_-filesonly-dev" })
if ($prod) {
$tags += @($bctags | ForEach-Object { "$_"; "$_-filesonly" })
}
$digests = $tags | ForEach-Object {
Write-Host -NoNewline "$_ : "
$manifest = docker manifest inspect mcr.microsoft.com/businesscentral:$_ -v | ConvertFrom-Json
$manifest.Descriptor.digest
} | Select-Object -Unique
Set-Location "generic"
$rootPath = Get-Location
$genericTag = (Get-Content -Raw -Path (Join-Path $RootPath 'tag.txt')).Trim(@(13,10,32))
$tagver = [System.Version]$genericTag
$revision = [int]($ENV:GITHUB_RUN_NUMBER)-[int]($ENV:RUNNUMBEROFFSET)
$genericTag = "$($tagver.Major).$($tagver.Minor).$($tagver.Build).$revision"
Write-Host "Using generic Tag $genericTag"
$webclient = New-Object System.Net.WebClient
$webclient.Headers.Add('Accept', "application/json")
$neededBcTags = $bctags | ForEach-Object {
$osVersion = [System.Version](($webclient.DownloadString("https://mcr.microsoft.com/v2/dotnet/framework/runtime/manifests/$($serverCoreTags."$_")") | ConvertFrom-Json).history[0].v1Compatibility | ConvertFrom-Json)."os.version"
"$osVersion-$genericTag|mcr.microsoft.com/dotnet/framework/runtime:$($serverCoreTags."$_")|$_"
"$osVersion-$genericTag-filesonly|mcr.microsoft.com/dotnet/framework/runtime:$($serverCoreTags."$_")|$_"
}
Write-Host "Needed Tags ($($neededBcTags.Count))"
$neededBcTags | ForEach-Object { Write-Host "- $_" }
$alltags = (($webclient.DownloadString("https://mcr.microsoft.com/v2/businesscentral/tags/list") | ConvertFrom-Json)).tags
$imagesBcTags = @($neededBcTags | Where-Object { $alltags -notcontains $_ })
Write-Host "Image Tags ($($imagesBcTags.Count))"
if ($imagesBcTags) {
$imagesBcTags | ForEach-Object { Write-Host "- $_" }
}
else {
Write-Host '- none'
}
$buildImagesJson = ConvertTo-Json -InputObject $imagesBcTags -Compress
$digestsJson = ConvertTo-Json -InputObject $digests -Compress
Add-Content -encoding utf8 -Path $ENV:GITHUB_OUTPUT -Value "digestsJson=$digestsJson"
Write-Host "digestsJson=$digestsJson"
Add-Content -encoding utf8 -Path $ENV:GITHUB_OUTPUT -Value "genericTag=$genericTag"
Write-Host "genericTag=$genericTag"
Add-Content -encoding utf8 -Path $ENV:GITHUB_OUTPUT -Value "buildImagesJson=$buildImagesJson"
Write-Host "buildImagesJson=$buildImagesJson"
}
catch {
Write-Host "::Error::Error analyzing images. Error was $($_.Exception.Message)"
$host.SetShouldExit(1)
}
[int] $revisionNumber = ([int]($ENV:GITHUB_RUN_NUMBER) - [int]($ENV:RUNNUMBEROFFSET))

. ${{ github.workspace }}/build/analyzeimages.ps1 `
-RevisionNumber $revisionNumber `
-PushToProd:$pushToProd

BuildImages:
runs-on: [ Windows-2025 ]
Expand All @@ -113,85 +62,29 @@ jobs:

- name: Build Image
env:
Tag: ${{ matrix.tag }}
PushToProd: ${{ github.event.inputs.PushToProd }}
GenericTag: ${{ needs.AnalyzeImages.outputs.genericTag }}
run: |
$erroractionpreference = "STOP"
Set-StrictMode -version 2.0
try {
$pushRegistry = "mcrbusinesscentral.azurecr.io"
az acr login --name $pushRegistry
Set-Location "generic"
$rootPath = Get-Location
$genericTag = $env:GenericTag
$pushToProd = $true
if ($env:GITHUB_EVENT_NAME -eq "workflow_dispatch") {
$filesonly = ($env:Tag -like '*-filesonly|*')
$only24 = ($env:Tag -like '*-24|*' -or $env:Tag -like '*-24-filesonly|*')
$baseImage = $env:Tag.split('|')[1]
$ltscTag = $env:Tag.split('|')[2]

$pushToProd = $true
if ($env:GITHUB_EVENT_NAME -eq "workflow_dispatch") {
$pushToProd = $env:PushToProd -eq 'True'
}
$osversion = '${{ matrix.tag }}'.split('|')[0].split('-')[0]
$filesonly = ('${{ matrix.tag }}' -like '*-filesonly|*')
$only24 = ('${{ matrix.tag }}' -like '*-24|*' -or '${{ matrix.tag }}' -like '*-24-filesonly|*')
$baseImage = '${{ matrix.tag }}'.split('|')[1]
$ltscTag = '${{ matrix.tag }}'.split('|')[2]
$setupUrlsFile = Join-Path $rootPath "Run/SetupUrls.ps1"
Get-Content -Path $setupUrlsFile | Out-Host
$dockerfile = Join-Path $rootPath "DOCKERFILE"
$strFilesOnly = ''
$str24 = ''
if ($only24) {
$str24 = "-24"
}
if ($filesOnly) {
$strFilesOnly = "-filesonly"
$dockerfile += '-filesonly'
}
$image = "my:$osversion-$genericTag$str24$strFilesOnly"
$newtags = @(
"$pushRegistry/public/businesscentral:$osversion$str24$strFilesonly-dev"
"$pushRegistry/public/businesscentral:$ltscTag$str24$strFilesonly-dev"
)
if ($pushToProd) {
$newtags += @(
"$pushRegistry/public/businesscentral:$osversion$str24$strFilesonly"
"$pushRegistry/public/businesscentral:$osversion-$genericTag$str24$strFilesonly"
"$pushRegistry/public/businesscentral:$ltscTag$str24$strFilesonly"
)
}
$newTags | out-host
$created = [DateTime]::Now.ToUniversalTime().ToString("yyyyMMddHHmm")
docker pull $baseimage
$inspect = docker inspect $baseimage | ConvertFrom-Json
$success = $false
docker build --build-arg baseimage=$baseimage `
--build-arg created=$created `
--build-arg tag="$genericTag" `
--build-arg osversion="$osversion" `
--build-arg filesonly="$filesonly" `
--build-arg only24="$only24" `
--isolation=hyperv `
--memory 8G `
--tag $image `
--file $dockerfile `
$RootPath | % {
$_ | Out-Host
if ($_ -like "Successfully built*") {
$success = $true
}
}
if (!$success) {
throw "Error building image"
}
$newtags | ForEach-Object {
Write-Host "Push $_"
docker tag $image $_
docker push $_
}
}
catch {
Write-Host "::Error::Error building images. Error was $($_.Exception.Message)"
$host.SetShouldExit(1)
}

. ${{ github.workspace }}/build/build.ps1 `
-BaseImage $baseImage `
-LtscTag $ltscTag `
-FilesOnly $filesonly `
-Only24 $only24 `
-GenericTag $env:GenericTag `
-PushToDev `
-PushToProd:$pushToProd

MarkOldImagesStale:
runs-on: [ Windows-Latest ]
needs: [ AnalyzeImages, BuildImages ]
Expand All @@ -215,16 +108,4 @@ jobs:
run: |
$erroractionpreference = "STOP"
$digests = $env:digestsJson | ConvertFrom-Json
$version = "1.2.0"
$filename = Join-Path $env:TEMP "oras_$($version)_windows_amd64.zip"
Invoke-RestMethod -Method GET -UseBasicParsing -Uri "https://github.com/oras-project/oras/releases/download/v$($version)/oras_$($version)_windows_amd64.zip" -OutFile $filename
Expand-Archive -Path $filename -DestinationPath temp
$pushRegistry = "mcrbusinesscentral.azurecr.io"
$staleDate = [System.DateTime]::Today.AddDays(-1).ToString('yyyy-MM-dd')
az acr login --name $pushRegistry
$digests | ForEach-Object {
$image = "$pushRegistry/public/businesscentral@$_"
Write-Host "Stale $image on $staleDate"
./temp/oras.exe attach --artifact-type application/vnd.microsoft.artifact.lifecycle --annotation "vnd.microsoft.artifact.lifecycle.end-of-life.date=$staleDate" $image
}

. ${{ github.workspace }}/build/markstale.ps1 -Digests $digests
14 changes: 1 addition & 13 deletions .github/workflows/MarkStale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,4 @@ jobs:
run: |
$erroractionpreference = "STOP"
$digests = "$env:digests".Split(",").Trim() | Select-Object -Unique
$version = "1.2.0"
$filename = Join-Path $env:TEMP "oras_$($version)_windows_amd64.zip"
Invoke-RestMethod -Method GET -UseBasicParsing -Uri "https://github.com/oras-project/oras/releases/download/v$($version)/oras_$($version)_windows_amd64.zip" -OutFile $filename
Expand-Archive -Path $filename -DestinationPath temp
$pushRegistry = "mcrbusinesscentral.azurecr.io"
$staleDate = [System.DateTime]::Today.AddDays(-1).ToString('yyyy-MM-dd')
az acr login --name $pushRegistry
$digests | ForEach-Object {
$image = "$pushRegistry/public/businesscentral@$_"
Write-Host "Stale $image on $staleDate"
./temp/oras.exe attach --artifact-type application/vnd.microsoft.artifact.lifecycle --annotation "vnd.microsoft.artifact.lifecycle.end-of-life.date=$staleDate" $image
}

. ${{ github.workspace }}/build/markstale.ps1 -Digests $digests
61 changes: 61 additions & 0 deletions .github/workflows/PRBuild.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Pull Request Build

on:
pull_request:
branches: [ master ]

permissions:
contents: read

defaults:
run:
shell: PowerShell

jobs:
AnalyzeImages:
runs-on: [ windows-latest ]
outputs:
genericTag: ${{ steps.Analyze.outputs.genericTag }}
buildImagesJson: ${{ steps.Analyze.outputs.buildImagesJson }}
digestsJson: ${{ steps.Analyze.outputs.digestsJson }}
steps:
- uses: actions/checkout@v4

- name: Analyze
id: Analyze
env:
RUNNUMBEROFFSET: ${{ vars.RUNNUMBEROFFSET }}
run: |
[int] $revisionNumber = ([int]($ENV:GITHUB_RUN_NUMBER) - [int]($ENV:RUNNUMBEROFFSET))

. ${{ github.workspace }}/build/analyzeimages.ps1 `
-RevisionNumber $revisionNumber

BuildImages:
runs-on: [ Windows-2025 ]
needs: [ AnalyzeImages ]
strategy:
matrix:
tag: ${{ fromJson(needs.AnalyzeImages.outputs.buildImagesJson) }}
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Build Image
env:
Tag: ${{ matrix.tag }}
GenericTag: ${{ needs.AnalyzeImages.outputs.genericTag }}
run: |
$osversion = $env:Tag.split('|')[0].split('-')[0]
$filesonly = ($env:Tag -like '*-filesonly|*')
$only24 = ($env:Tag -like '*-24|*' -or $env:Tag -like '*-24-filesonly|*')
$baseImage = $env:Tag.split('|')[1]
$ltscTag = $env:Tag.split('|')[2]

. ${{ github.workspace }}/build/build.ps1 `
-BaseImage $baseImage `
-LtscTag $ltscTag `
-FilesOnly $filesonly `
-Only24 $only24 `
-GenericTag $env:GenericTag
28 changes: 28 additions & 0 deletions LOCAL_DEV_ENV.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# How to build and test images locally

## Prerequisites
- Install and run [Docker Desktop](https://docs.docker.com/desktop/install/windows-install/). Make sure it is running Windows containers.
- Install [BcContainerHelper PS module](https://www.powershellgallery.com/packages/BcContainerHelper) (latest available version).
`Install-Module BCContainerHelper -AllowPrerelease` would do.

## Build a new image locally

If you want to build a new docker image locally you can use the build.ps1 script. The following will produce a new docker image based on Windows Server Core 2025.
```powershell
$GenericTag = "1.2.3.4"
$baseImage = "mcr.microsoft.com/dotnet/framework/runtime:4.8.1-windowsservercore-ltsc2025"
$imageName = ./build/build.ps1 -BaseImage $baseImage -LtscTag 'ltsc2025' -FilesOnly $false -Only24 $false -GenericTag $GenericTag
```

If you'd rather use a different base image, you can also use one of the following:
* mcr.microsoft.com/dotnet/framework/runtime:4.8-windowsservercore-ltsc2016
* mcr.microsoft.com/dotnet/framework/runtime:4.8-windowsservercore-ltsc2019
* mcr.microsoft.com/dotnet/framework/runtime:4.8.1-windowsservercore-ltsc2022
* mcr.microsoft.com/dotnet/framework/runtime:4.8.1-windowsservercore-ltsc2025

## Create a docker container with the image

Once you have a local image you can use New-BCContainer to spin up a Business Central container that uses your image.
```powershell
New-BcContainer -accept_eula -accept_insiderEula -containerName "MyTestContainer" -artifactUrl (Get-BCArtifactUrl -select NextMajor -accept_insiderEula -country W1) -useGenericImage $imageName
```
88 changes: 88 additions & 0 deletions build/analyzeimages.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
param(
[Parameter(Mandatory = $false)]
[int] $RevisionNumber,
[Parameter(Mandatory = $false)]
[switch] $PushToProd
)

try {
$repositoryRoot = Join-Path $PSScriptRoot "..\"
Push-Location (Join-Path $repositoryRoot "generic")
$rootPath = Get-Location

# List of the base image tags used for the Business Central images
# https://mcr.microsoft.com/en-us/artifact/mar/dotnet/framework/runtime
$baseImage = "dotnet/framework/runtime"
$baseImageTags = @{
"ltsc2016" = "4.8-windowsservercore-ltsc2016"
"ltsc2019" = "4.8-windowsservercore-ltsc2019"
"ltsc2022" = "4.8.1-windowsservercore-ltsc2022"
"ltsc2025" = "4.8.1-windowsservercore-ltsc2025"
}

# List of the Business Central tags to be used
$bctags = @('ltsc2016', 'ltsc2019', 'ltsc2022', 'ltsc2025')

# Get the tags for the Business Central images
$tags = @($bctags | ForEach-Object { "$_-dev"; "$_-filesonly-dev" })
if ($PushToProd) {
$tags += @($bctags | ForEach-Object { "$_"; "$_-filesonly" })
}

# Get the digests for the tags from the Microsoft Container Registry
$digests = $tags | ForEach-Object {
$tag = $_
$manifest = docker manifest inspect mcr.microsoft.com/businesscentral:$tag -v | ConvertFrom-Json
$digest = $manifest.Descriptor.digest
Write-Host "Digest for tag $($tag): $digest" -ForegroundColor Cyan
return $manifest.Descriptor.digest
} | Select-Object -Unique
Write-Host "Found $($digests.Count) digests will be marked as stale:"

# Get the generic tag to use for the images
$genericTag = (Get-Content -Raw -Path (Join-Path $RootPath 'tag.txt')).Trim(@(13, 10, 32))
$tagver = [System.Version]$genericTag
$genericTag = "$($tagver.Major).$($tagver.Minor).$($tagver.Build).$RevisionNumber"
Write-Host "Using generic Tag $genericTag"


# Build a list of strings that contain the necessary information to build the Business Central images
# The format is: "osVersion-genericTag|mcr.microsoft.com/baseImage:baseImageTag|bctag"
# This will be used in the later steps to build the images
$webclient = New-Object System.Net.WebClient
$webclient.Headers.Add('Accept', "application/json")
$neededBcTags = $bctags | ForEach-Object {
$osVersion = [System.Version](($webclient.DownloadString("https://mcr.microsoft.com/v2/$baseImage/manifests/$($baseImageTags."$_")") | ConvertFrom-Json).history[0].v1Compatibility | ConvertFrom-Json)."os.version"
"$osVersion-$genericTag|mcr.microsoft.com/$($baseImage):$($baseImageTags."$_")|$_"
"$osVersion-$genericTag-filesonly|mcr.microsoft.com/$($baseImage):$($baseImageTags."$_")|$_"
}
Write-Host "Needed Tags ($($neededBcTags.Count))"
$neededBcTags | ForEach-Object { Write-Host "- $_" }
$alltags = (($webclient.DownloadString("https://mcr.microsoft.com/v2/businesscentral/tags/list") | ConvertFrom-Json)).tags
$imagesBcTags = @($neededBcTags | Where-Object { $alltags -notcontains $_ })
Write-Host "Image Tags ($($imagesBcTags.Count))"
if ($imagesBcTags) {
$imagesBcTags | ForEach-Object { Write-Host "- $_" }
}
else {
Write-Host '- none'
}

# Output digests, generic tag and build images as JSON to be used in the GitHub Actions workflow
$buildImagesJson = ConvertTo-Json -InputObject $imagesBcTags -Compress
$digestsJson = ConvertTo-Json -InputObject $digests -Compress
Write-Host "genericTag=$genericTag" -ForegroundColor Green
Write-Host "digestsJson=$digestsJson" -ForegroundColor Green
Write-Host "buildImagesJson=$buildImagesJson" -ForegroundColor Green

# Check if this is running in a GitHub Actions environment
if ($ENV:GITHUB_OUTPUT) {
Add-Content -encoding utf8 -Path $ENV:GITHUB_OUTPUT -Value "genericTag=$genericTag"
Add-Content -encoding utf8 -Path $ENV:GITHUB_OUTPUT -Value "digestsJson=$digestsJson"
Add-Content -encoding utf8 -Path $ENV:GITHUB_OUTPUT -Value "buildImagesJson=$buildImagesJson"
}

}
finally {
Pop-Location
}
Loading