Skip to content
77 changes: 77 additions & 0 deletions .github/workflows/release-smoke-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Release Smoke Test

on:
pull_request:
branches: [ "main" ]
paths:
- '.github/workflows/release-smoke-test.yml'
- 'testing/release-smoke/**'
workflow_dispatch:
inputs:
release-repository:
description: 'GitHub repository to download the release from'
required: true
default: 'Devolutions/UniGetUI'
release-tag:
description: 'Release tag to test. Leave empty for the latest release.'
required: false
default: ''
installer-asset-name:
description: 'Installer asset name in the GitHub release'
required: true
default: 'UniGetUI.Installer.x64.exe'
max-stage:
description: 'Last smoke-test stage to run'
required: true
type: choice
default: 'install-launch'
options:
- rdp
- rdp-client
- remoting-server
- remote-command
- install-launch

jobs:
release-smoke-test:
name: Windows release smoke test
runs-on: windows-latest
timeout-minutes: 90
permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Run release smoke test
shell: pwsh
env:
GITHUB_TOKEN: ${{ github.token }}
RELEASE_REPOSITORY: ${{ github.event.inputs['release-repository'] || 'Devolutions/UniGetUI' }}
RELEASE_TAG: ${{ github.event.inputs['release-tag'] || '' }}
INSTALLER_ASSET_NAME: ${{ github.event.inputs['installer-asset-name'] || 'UniGetUI.Installer.x64.exe' }}
MAX_STAGE: ${{ github.event.inputs['max-stage'] || 'install-launch' }}
run: |
.\testing\release-smoke\Invoke-ReleaseSmokeTest.ps1 `
-ReleaseRepository $env:RELEASE_REPOSITORY `
-ReleaseTag $env:RELEASE_TAG `
-InstallerAssetName $env:INSTALLER_ASSET_NAME `
-MaxStage $env:MAX_STAGE `
-ArtifactsDir '${{ github.workspace }}\artifacts\release-smoke'

- name: Cleanup release smoke test
if: always()
shell: pwsh
run: |
.\testing\release-smoke\Invoke-ReleaseSmokeTest.ps1 `
-CleanupOnly `
-ArtifactsDir '${{ github.workspace }}\artifacts\release-smoke'

- name: Upload release smoke artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: release-smoke-test
path: artifacts\release-smoke
if-no-files-found: warn
172 changes: 172 additions & 0 deletions testing/release-smoke/Enable-LocalRdp.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
[CmdletBinding(DefaultParameterSetName = 'Enable')]
param(
[Parameter(ParameterSetName = 'Enable')]
[Parameter(ParameterSetName = 'Cleanup')]
[string] $StatePath = (Join-Path $env:RUNNER_TEMP 'unigetui-release-smoke-rdp-state.json'),

[Parameter(ParameterSetName = 'Cleanup')]
[switch] $Cleanup
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$terminalServerPath = 'HKLM:\System\CurrentControlSet\Control\Terminal Server'
$rdpTcpPath = Join-Path $terminalServerPath 'WinStations\RDP-Tcp'
$rdpGroupName = 'Remote Desktop Users'

function Get-RegistryValue {
param(
[Parameter(Mandatory)]
[string] $Path,

[Parameter(Mandatory)]
[string] $Name
)

$property = Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue
if ($null -eq $property) {
return $null
}

return $property.$Name
}

function Write-JsonFile {
param(
[Parameter(Mandatory)]
[string] $Path,

[Parameter(Mandatory)]
[object] $Value
)

$directory = Split-Path -Path $Path -Parent
New-Item -Path $directory -ItemType Directory -Force | Out-Null
$Value | ConvertTo-Json -Depth 8 | Set-Content -Path $Path -Encoding utf8NoBOM
}

function Test-TcpPort {
param(
[Parameter(Mandatory)]
[string] $HostName,

[Parameter(Mandatory)]
[int] $Port
)

$client = [System.Net.Sockets.TcpClient]::new()
try {
$connect = $client.BeginConnect($HostName, $Port, $null, $null)
if (-not $connect.AsyncWaitHandle.WaitOne([TimeSpan]::FromSeconds(1))) {
return $false
}

$client.EndConnect($connect)
return $true
}
catch {
return $false
}
finally {
$client.Dispose()
}
}

function Wait-TcpPort {
param(
[Parameter(Mandatory)]
[string] $HostName,

[Parameter(Mandatory)]
[int] $Port,

[int] $TimeoutSeconds = 30
)

$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
do {
if (Test-TcpPort -HostName $HostName -Port $Port) {
return
}

Start-Sleep -Seconds 1
} while ((Get-Date) -lt $deadline)

throw "Timed out waiting for $HostName`:$Port to accept TCP connections."
}

if ($Cleanup) {
if (-not (Test-Path $StatePath)) {
return
}

$state = Get-Content -Path $StatePath -Raw | ConvertFrom-Json

if ($null -ne $state.fDenyTSConnections) {
Set-ItemProperty -Path $terminalServerPath -Name 'fDenyTSConnections' -Value ([int] $state.fDenyTSConnections)
}

if ($null -ne $state.UserAuthentication) {
Set-ItemProperty -Path $rdpTcpPath -Name 'UserAuthentication' -Value ([int] $state.UserAuthentication)
}

foreach ($rule in @($state.FirewallRules)) {
if ($null -ne $rule.Name -and $null -ne $rule.Enabled) {
Set-NetFirewallRule -Name $rule.Name -Enabled $rule.Enabled -ErrorAction SilentlyContinue
}
}

if ($state.AddedToRemoteDesktopUsers) {
Remove-LocalGroupMember -Group $rdpGroupName -Member $state.LocalUserName -ErrorAction SilentlyContinue
}

Remove-Item -Path $StatePath -Force -ErrorAction SilentlyContinue
return
}

$localUserName = $env:USERNAME
if ([string]::IsNullOrWhiteSpace($localUserName)) {
throw 'USERNAME is not set; cannot configure a local RDP user.'
}

$passwordBytes = [System.Security.Cryptography.RandomNumberGenerator]::GetBytes(24)
$temporaryPassword = 'RdpSmoke!' + [Convert]::ToBase64String($passwordBytes) + 'aA1!'
Write-Host "::add-mask::$temporaryPassword"

$currentMembers = @(Get-LocalGroupMember -Group $rdpGroupName -ErrorAction SilentlyContinue | ForEach-Object { $_.Name })
$memberNames = @($localUserName, "$env:COMPUTERNAME\$localUserName")
$wasRdpMember = [bool]($currentMembers | Where-Object { $memberNames -contains $_ } | Select-Object -First 1)

$state = [pscustomobject]@{
LocalUserName = $localUserName
DomainUserName = "$env:COMPUTERNAME\$localUserName"
fDenyTSConnections = Get-RegistryValue -Path $terminalServerPath -Name 'fDenyTSConnections'
UserAuthentication = Get-RegistryValue -Path $rdpTcpPath -Name 'UserAuthentication'
FirewallRules = @(Get-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction SilentlyContinue | Select-Object -Property Name, Enabled)
AddedToRemoteDesktopUsers = (-not $wasRdpMember)
}
Write-JsonFile -Path $StatePath -Value $state

$securePassword = ConvertTo-SecureString -String $temporaryPassword -AsPlainText -Force
Set-LocalUser -Name $localUserName -Password $securePassword

if (-not $wasRdpMember) {
Add-LocalGroupMember -Group $rdpGroupName -Member $localUserName
}

Set-ItemProperty -Path $terminalServerPath -Name 'fDenyTSConnections' -Value 0
Set-ItemProperty -Path $rdpTcpPath -Name 'UserAuthentication' -Value 0
Set-Service -Name TermService -StartupType Automatic
Start-Service -Name TermService
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' | Out-Null
Wait-TcpPort -HostName '127.0.0.1' -Port 3389

[pscustomobject]@{
UserName = $localUserName
DomainUserName = "$env:COMPUTERNAME\$localUserName"
Password = $temporaryPassword
HostName = '127.0.0.1'
Port = 3389
StatePath = $StatePath
} | ConvertTo-Json -Compress
Loading
Loading