#Requires -Version 5.1 [CmdletBinding()] param( [Parameter(Mandatory)] [ValidatePattern('^\d+\.\d+\.\d+([-.][0-9A-Za-z.-]+)?$')] [string]$Version, [string]$OutputRoot = (Join-Path $PSScriptRoot '..\artifacts\releases'), [string]$ServerContentPath, [switch]$SkipBuild ) $ErrorActionPreference = 'Stop' $repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path $resolvedOutputRoot = [IO.Path]::GetFullPath($OutputRoot) if (-not $resolvedOutputRoot.StartsWith($repositoryRoot + '\', [StringComparison]::OrdinalIgnoreCase)) { throw 'OutputRoot must be beneath the repository root.' } function Copy-RequiredFile { param( [Parameter(Mandatory)][string]$Source, [Parameter(Mandatory)][string]$Destination ) if (-not (Test-Path -LiteralPath $Source -PathType Leaf)) { throw "Required package input is missing: $Source" } New-Item -ItemType Directory -Path (Split-Path $Destination -Parent) -Force | Out-Null Copy-Item -LiteralPath $Source -Destination $Destination -Force } function Write-PackageManifest { param( [Parameter(Mandatory)][string]$PackageRoot, [Parameter(Mandatory)][string]$PackageVersion, [Parameter(Mandatory)][string]$PackageKind, [ValidateSet('Windows10Legacy', 'Windows11Modern')] [string]$CompatibilityProfile, [string]$TargetOperatingSystem ) $resolvedPackageRoot = (Resolve-Path -LiteralPath $PackageRoot).Path.TrimEnd('\') $files = @(Get-ChildItem -LiteralPath $resolvedPackageRoot -Recurse -File | Where-Object Name -ne 'package-manifest.json' | Sort-Object FullName | ForEach-Object { [ordered]@{ Path = $_.FullName.Substring($resolvedPackageRoot.Length).TrimStart('\') Sha256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash Length = $_.Length } }) $manifest = [ordered]@{ SchemaVersion = 2 Product = 'SGU Credential Provider' PackageKind = $PackageKind Version = $PackageVersion CreatedAt = (Get-Date).ToUniversalTime().ToString('o') Files = $files } if ($CompatibilityProfile) { $manifest['CompatibilityProfile'] = $CompatibilityProfile } if ($TargetOperatingSystem) { $manifest['TargetOperatingSystem'] = $TargetOperatingSystem } [IO.File]::WriteAllText( (Join-Path $resolvedPackageRoot 'package-manifest.json'), ($manifest | ConvertTo-Json -Depth 6), [Text.UTF8Encoding]::new($false)) } if (-not $SkipBuild) { & (Join-Path $PSScriptRoot 'Publish-Lab.ps1') -Configuration Release ` -OutputRoot (Join-Path $repositoryRoot 'artifacts') | Out-Null } $brokerOutput = Join-Path $repositoryRoot 'artifacts\broker' $providerOutput = Join-Path $repositoryRoot 'artifacts\credential-provider' $prerequisiteRoot = Join-Path $repositoryRoot 'artifacts\prerequisites' $runtimeInstaller = Get-ChildItem -LiteralPath $prerequisiteRoot -Filter '*x64*.exe' ` -File -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1 if (-not $runtimeInstaller) { throw 'Place the offline Microsoft .NET 10 x64 runtime installer in artifacts\prerequisites.' } New-Item -ItemType Directory -Path $resolvedOutputRoot -Force | Out-Null $windows11ClientRoot = Join-Path $resolvedOutputRoot "sgu-windows11-client-bootstrap-$Version" $windows10ClientRoot = Join-Path $resolvedOutputRoot "sgu-windows10-legacy-client-bootstrap-$Version" $clientRoot = $windows11ClientRoot $serverRoot = Join-Path $resolvedOutputRoot "sgu-server-bootstrap-$Version" $linuxClientRoot = Join-Path $resolvedOutputRoot "sgu-linux-client-bootstrap-$Version" $azureRoot = Join-Path $resolvedOutputRoot "sgu-azure-infrastructure-$Version" $windows11ClientZip = "$windows11ClientRoot.zip" $windows10ClientZip = "$windows10ClientRoot.zip" $serverZip = "$serverRoot.zip" $linuxClientZip = "$linuxClientRoot.zip" $azureZip = "$azureRoot.zip" foreach ($target in @( $windows11ClientRoot,$windows10ClientRoot,$serverRoot,$linuxClientRoot,$azureRoot, $windows11ClientZip,$windows10ClientZip,$serverZip,$linuxClientZip,$azureZip)) { if (Test-Path -LiteralPath $target) { throw "Release target already exists: $target" } } New-Item -ItemType Directory ` -Path $windows11ClientRoot,$windows10ClientRoot,$serverRoot,$linuxClientRoot,$azureRoot ` -Force | Out-Null $welcomeFontNames = @( 'IndivisaTextSans-Regular.otf', 'IndivisaTextSans-Bold.otf', 'IndivisaTextSans-BoldItalic.otf', 'IndivisaTextSerif-Regular.otf', 'IndivisaTextSerif-BoldItalic.otf' ) Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Invoke-SguClientBootstrap.ps1') ` -Destination (Join-Path $clientRoot 'Invoke-SguClientBootstrap.ps1') Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguClientEnrollment.cmd') ` -Destination (Join-Path $clientRoot 'Start-SguClientEnrollment.cmd') $clientScripts = @( 'Enable-LabRemoteAccess.ps1', 'Enable-SguClientMonitoring.ps1', 'Enroll-SguDomainClient.ps1', 'Install-CredentialProvider.ps1', 'Install-SguEnrollmentGuard.ps1', 'Install-SguRustDeskClient.ps1', 'Register-SguClientCertificate.ps1', 'Repair-SguClientEnrollment.ps1', 'Set-SguStandardLocalUser.ps1', 'Test-SguClientEnrollment.ps1' ) foreach ($scriptName in $clientScripts) { Copy-RequiredFile -Source (Join-Path $PSScriptRoot $scriptName) ` -Destination (Join-Path $clientRoot "payload\scripts\$scriptName") } Copy-Item -Path (Join-Path $providerOutput '*') ` -Destination (New-Item -ItemType Directory ` -Path (Join-Path $clientRoot 'payload\credential-provider') -Force).FullName ` -Recurse -Force Copy-RequiredFile -Source (Join-Path $repositoryRoot 'assets\branding\lasalle-mascot-account.png') ` -Destination (Join-Path $clientRoot 'payload\credential-provider\branding\user.png') Copy-RequiredFile -Source (Join-Path $repositoryRoot 'assets\branding\darkblue.jpg') ` -Destination (Join-Path $clientRoot 'payload\credential-provider\branding\darkblue.jpg') Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Set-SguWelcomeWallpaper.ps1') ` -Destination (Join-Path $clientRoot 'payload\credential-provider\branding\Set-SguWelcomeWallpaper.ps1') foreach ($fontName in $welcomeFontNames) { Copy-RequiredFile -Source (Join-Path $repositoryRoot "assets\branding\fonts\$fontName") ` -Destination (Join-Path $clientRoot "payload\credential-provider\branding\fonts\$fontName") } Copy-RequiredFile -Source $runtimeInstaller.FullName ` -Destination (Join-Path $clientRoot "payload\prerequisites\$($runtimeInstaller.Name)") # Both Windows packages share the provider and enrollment implementation. The # Windows 10 artifact freezes the direct-network compatibility surface, while # the Windows 11 artifact adds the modern Azure P2S/pre-logon entry point. Copy-Item -Path (Join-Path $windows11ClientRoot '*') ` -Destination $windows10ClientRoot -Recurse -Force Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguAzureClientEnrollment.cmd') ` -Destination (Join-Path $windows11ClientRoot 'Start-SguAzureClientEnrollment.cmd') Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Install-SguAzureP2sClient.ps1') ` -Destination (Join-Path $windows11ClientRoot 'Install-SguAzureP2sClient.ps1') Write-PackageManifest -PackageRoot $windows10ClientRoot -PackageVersion $Version ` -PackageKind WindowsClient -CompatibilityProfile Windows10Legacy ` -TargetOperatingSystem 'Windows 10 Pro, Enterprise, or Education (build below 22000)' Write-PackageManifest -PackageRoot $windows11ClientRoot -PackageVersion $Version ` -PackageKind WindowsClient -CompatibilityProfile Windows11Modern ` -TargetOperatingSystem 'Windows 11 Pro, Enterprise, or Education (build 22000 or later)' Compress-Archive -Path (Join-Path $windows10ClientRoot '*') -DestinationPath $windows10ClientZip ` -CompressionLevel Optimal Compress-Archive -Path (Join-Path $windows11ClientRoot '*') -DestinationPath $windows11ClientZip ` -CompressionLevel Optimal # Linux clients use their native PAM/SSSD sign-in stack rather than the Windows # Credential Provider. Keep their self-contained bootstrap independent so a # Linux administrator never receives Windows binaries or certificate material. Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Enroll-SguLinuxDomainClient.sh') ` -Destination (Join-Path $linuxClientRoot 'Enroll-SguLinuxDomainClient.sh') Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Install-SguLinuxRustDeskClient.sh') ` -Destination (Join-Path $linuxClientRoot 'Install-SguLinuxRustDeskClient.sh') Copy-RequiredFile -Source (Join-Path $repositoryRoot 'docs\linux-client-enrollment.md') ` -Destination (Join-Path $linuxClientRoot 'README.md') Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Set-SguWelcomeWallpaper.sh') ` -Destination (Join-Path $linuxClientRoot 'welcome-wallpaper\Set-SguWelcomeWallpaper.sh') Copy-RequiredFile -Source (Join-Path $repositoryRoot 'assets\branding\darkblue.jpg') ` -Destination (Join-Path $linuxClientRoot 'welcome-wallpaper\darkblue.jpg') foreach ($fontName in $welcomeFontNames) { Copy-RequiredFile -Source (Join-Path $repositoryRoot "assets\branding\fonts\$fontName") ` -Destination (Join-Path $linuxClientRoot "welcome-wallpaper\fonts\$fontName") } Write-PackageManifest -PackageRoot $linuxClientRoot -PackageVersion $Version -PackageKind LinuxClient Compress-Archive -Path (Join-Path $linuxClientRoot '*') -DestinationPath $linuxClientZip ` -CompressionLevel Optimal Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Initialize-SguDomainController.ps1') ` -Destination (Join-Path $serverRoot 'Initialize-SguDomainController.ps1') Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguServerBootstrap.cmd') ` -Destination (Join-Path $serverRoot 'Start-SguServerBootstrap.cmd') Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguAzureServerBootstrap.cmd') ` -Destination (Join-Path $serverRoot 'Start-SguAzureServerBootstrap.cmd') $serverScripts = @( 'Deploy-AuthBroker.ps1', 'Enable-SguServerRemoteManagement.ps1', 'Get-SguUsageReport.ps1', 'Get-SguBrokerLog.ps1', 'Install-SguDomainMonitoring.ps1', 'Install-SguRustDeskClient.ps1', 'Install-SguRustDeskLinuxEnrollment.ps1', 'Install-SguRustDeskServer.ps1', 'Invoke-SguRustDeskLinuxRegistrationProcessor.ps1', 'Invoke-SguMonitoringMaintenance.ps1', 'New-LabCertificate.ps1', 'Get-SguRustDeskDevice.ps1', 'Register-SguClientCertificate.ps1', 'Register-SguRustDeskDevice.ps1', 'Set-LabBrokerDns.ps1', 'Set-SguDomainComputerPolicies.ps1', 'Set-SguDomainUserPolicies.ps1' ) foreach ($scriptName in $serverScripts) { Copy-RequiredFile -Source (Join-Path $PSScriptRoot $scriptName) ` -Destination (Join-Path $serverRoot "payload\scripts\$scriptName") } Copy-Item -Path (Join-Path $brokerOutput '*') ` -Destination (New-Item -ItemType Directory ` -Path (Join-Path $serverRoot 'payload\broker') -Force).FullName ` -Recurse -Force $serverContentTarget = Join-Path $serverRoot 'payload\server-content\Packages' New-Item -ItemType Directory -Path $serverContentTarget -Force | Out-Null if ($ServerContentPath) { if (-not (Test-Path -LiteralPath $ServerContentPath -PathType Container)) { throw 'ServerContentPath does not exist.' } Copy-Item -Path (Join-Path $ServerContentPath '*') ` -Destination $serverContentTarget -Recurse -Force } Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Set-SguWelcomeWallpaper.ps1') ` -Destination (Join-Path $serverContentTarget 'welcome-wallpaper\Set-SguWelcomeWallpaper.ps1') Copy-RequiredFile -Source (Join-Path $repositoryRoot 'assets\branding\darkblue.jpg') ` -Destination (Join-Path $serverContentTarget 'welcome-wallpaper\darkblue.jpg') foreach ($fontName in $welcomeFontNames) { Copy-RequiredFile -Source (Join-Path $repositoryRoot "assets\branding\fonts\$fontName") ` -Destination (Join-Path $serverContentTarget "welcome-wallpaper\fonts\$fontName") } Write-PackageManifest -PackageRoot $serverRoot -PackageVersion $Version -PackageKind Server Compress-Archive -Path (Join-Path $serverRoot '*') -DestinationPath $serverZip ` -CompressionLevel Optimal # Azure infrastructure is packaged separately because it runs on the trusted # administrator workstation, not inside the domain controller or a client. $azureScriptsRoot = Join-Path $azureRoot 'scripts' $azureInfrastructureRoot = Join-Path $azureRoot 'infra\azure' New-Item -ItemType Directory -Path $azureScriptsRoot,$azureInfrastructureRoot -Force | Out-Null Copy-RequiredFile -Source (Join-Path $repositoryRoot 'infra\azure\main.bicep') ` -Destination (Join-Path $azureInfrastructureRoot 'main.bicep') foreach ($scriptName in @( 'New-SguAzureP2sCertificates.ps1', 'Deploy-SguAzureInfrastructure.ps1', 'Get-SguAzureP2sPackage.ps1', 'Install-SguAzureP2sClient.ps1')) { Copy-RequiredFile -Source (Join-Path $PSScriptRoot $scriptName) ` -Destination (Join-Path $azureScriptsRoot $scriptName) } Copy-RequiredFile -Source (Join-Path $repositoryRoot 'docs\azure-vpn-deployment.md') ` -Destination (Join-Path $azureRoot 'README.md') Write-PackageManifest -PackageRoot $azureRoot -PackageVersion $Version -PackageKind AzureInfrastructure Compress-Archive -Path (Join-Path $azureRoot '*') -DestinationPath $azureZip ` -CompressionLevel Optimal $checksums = @( ("{0} {1}" -f (Get-FileHash -LiteralPath $windows10ClientZip -Algorithm SHA256).Hash, (Split-Path $windows10ClientZip -Leaf)) ("{0} {1}" -f (Get-FileHash -LiteralPath $windows11ClientZip -Algorithm SHA256).Hash, (Split-Path $windows11ClientZip -Leaf)) ("{0} {1}" -f (Get-FileHash -LiteralPath $serverZip -Algorithm SHA256).Hash, (Split-Path $serverZip -Leaf)) ("{0} {1}" -f (Get-FileHash -LiteralPath $linuxClientZip -Algorithm SHA256).Hash, (Split-Path $linuxClientZip -Leaf)) ("{0} {1}" -f (Get-FileHash -LiteralPath $azureZip -Algorithm SHA256).Hash, (Split-Path $azureZip -Leaf)) ) $checksumsPath = Join-Path $resolvedOutputRoot "SHA256SUMS-$Version.txt" [IO.File]::WriteAllLines($checksumsPath, $checksums, [Text.UTF8Encoding]::new($false)) [pscustomobject]@{ Version = $Version Windows10LegacyClientPackage = $windows10ClientZip Windows10LegacyClientSha256 = (Get-FileHash -LiteralPath $windows10ClientZip -Algorithm SHA256).Hash Windows11ClientPackage = $windows11ClientZip Windows11ClientSha256 = (Get-FileHash -LiteralPath $windows11ClientZip -Algorithm SHA256).Hash LinuxClientPackage = $linuxClientZip LinuxClientSha256 = (Get-FileHash -LiteralPath $linuxClientZip -Algorithm SHA256).Hash ServerPackage = $serverZip ServerSha256 = (Get-FileHash -LiteralPath $serverZip -Algorithm SHA256).Hash AzureInfrastructurePackage = $azureZip AzureInfrastructureSha256 = (Get-FileHash -LiteralPath $azureZip -Algorithm SHA256).Hash Checksums = $checksumsPath RuntimeInstaller = $runtimeInstaller.Name }