diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml new file mode 100644 index 00000000..c3484ce5 --- /dev/null +++ b/.github/workflows/install.yml @@ -0,0 +1,650 @@ +name: Installation + +on: + workflow_call: + inputs: + name: + description: Display name for the Windows version + required: true + type: string + + version: + description: VERSION value passed to the container + required: true + type: string + + runner: + description: GitHub Actions runner + required: false + default: ubuntu-24.04 + type: string + + image: + description: Container image to test + required: false + default: ghcr.io/dockur/windows:latest + type: string + + callback: + description: Guest script type + required: false + default: powershell + type: string + + cpu: + description: CPU model exposed to Windows + required: false + default: host + type: string + + expected_caption: + description: Text expected in the Windows caption + required: true + type: string + + expected_edition: + description: Expected Windows EditionID + required: false + default: "" + type: string + + minimum_build: + description: Minimum acceptable Windows build number + required: true + type: number + + platform: + description: Expected Windows platform + required: true + type: string + +permissions: + contents: read + +jobs: + install: + name: ${{ inputs.name }} + runs-on: ${{ inputs.runner }} + timeout-minutes: 180 + + env: + CONTAINER: windows-test + IMAGE: ${{ inputs.image }} + + steps: + - name: Check KVM + shell: bash + run: | + set -Eeuo pipefail + + test -c /dev/kvm + + - name: Free disk space + shell: bash + run: | + set -Eeuo pipefail + + sudo rm -rf \ + /usr/local/.ghcup \ + /usr/local/lib/android \ + /usr/local/lib/node_modules \ + /usr/local/share/boost \ + /usr/local/share/chromium \ + /usr/local/share/powershell \ + /usr/share/dotnet \ + /usr/share/swift \ + /opt/az \ + /opt/ghc \ + /opt/google \ + /opt/hostedtoolcache \ + /opt/microsoft \ + /opt/pipx + + sudo apt-get clean + sudo rm -rf \ + /var/cache/apt/* \ + /var/lib/apt/lists/* + + docker system prune \ + --all \ + --force \ + --volumes > /dev/null || true + + available_kb="$(df --output=avail / | tail -1)" + available_gb="$((available_kb / 1024 / 1024))" + + echo "Available disk space: ${available_gb} GB" + + if (( available_gb < 40 )); then + echo "At least 40 GB of free space is required." + exit 1 + fi + + - name: Prepare installation test + id: test + shell: bash + env: + CALLBACK: ${{ inputs.callback }} + run: | + set -Eeuo pipefail + + case "$CALLBACK" in + powershell | legacy) + ;; + *) + echo "Unsupported guest script type: $CALLBACK" + exit 1 + ;; + esac + + token="$(cat /proc/sys/kernel/random/uuid)" + + mkdir -p \ + "$RUNNER_TEMP/data" \ + "$RUNNER_TEMP/oem" \ + "$RUNNER_TEMP/storage" + + printf '%s\n' "$token" > "$RUNNER_TEMP/data/readme.txt" + + case "$CALLBACK" in + powershell) + cat > "$RUNNER_TEMP/oem/ready.ps1" <<'POWERSHELL' + param( + [Parameter(Mandatory = $true)] + [string]$Token + ) + + $ErrorActionPreference = "Stop" + + $windows = Get-CimInstance Win32_OperatingSystem + $registry = Get-ItemProperty ` + "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" + + $platform = switch ( + $env:PROCESSOR_ARCHITECTURE.ToUpperInvariant() + ) { + "AMD64" { + "x64" + } + "ARM64" { + "arm64" + } + "X86" { + "x86" + } + default { + $env:PROCESSOR_ARCHITECTURE.ToLowerInvariant() + } + } + + while ($true) { + try { + $share = ( + Get-Content ` + -LiteralPath "\\host.lan\Data\readme.txt" ` + -Raw + ).Trim() + + $result = @{ + token = $Token + caption = [string]$windows.Caption + edition = [string]$registry.EditionID + version = [string]$windows.Version + build = [string]$windows.BuildNumber + platform = $platform + share = $share + } + + $json = $result | ConvertTo-Json -Compress + $temporary = "\\host.lan\Data\windows.tmp" + $destination = "\\host.lan\Data\windows.json" + $encoding = New-Object System.Text.UTF8Encoding($false) + + [System.IO.File]::WriteAllText( + $temporary, + $json, + $encoding + ) + + Move-Item ` + -LiteralPath $temporary ` + -Destination $destination ` + -Force + + break + } + catch { + Start-Sleep -Seconds 10 + } + } + POWERSHELL + + cat > "$RUNNER_TEMP/oem/install.bat" < "$RUNNER_TEMP/oem/ready.vbs" <<'VBSCRIPT' + Option Explicit + + Const ForReading = 1 + Const ForWriting = 2 + + Dim arguments + Dim token + + Set arguments = WScript.Arguments + + If arguments.Count < 1 Then + WScript.Quit 1 + End If + + token = arguments(0) + + Function EscapeJson(value) + Dim result + + result = CStr(value) + result = Replace(result, "\", "\\") + result = Replace(result, Chr(34), "\" & Chr(34)) + result = Replace(result, vbCr, "\r") + result = Replace(result, vbLf, "\n") + result = Replace(result, vbTab, "\t") + + EscapeJson = result + End Function + + Function ReadRegistry(shell, path) + Dim value + + value = "" + + On Error Resume Next + value = shell.RegRead(path) + + If Err.Number <> 0 Then + Err.Clear + value = "" + End If + + On Error GoTo 0 + + ReadRegistry = CStr(value) + End Function + + Function ReadTextFile(filesystem, path) + Dim file + Dim value + + value = "" + + Set file = filesystem.OpenTextFile(path, ForReading, False) + value = file.ReadAll + file.Close + + value = Replace(value, vbCr, "") + value = Replace(value, vbLf, "") + + ReadTextFile = value + End Function + + Function GetPlatform(shell) + Dim architecture + + architecture = UCase(shell.ExpandEnvironmentStrings("%PROCESSOR_ARCHITECTURE%")) + + Select Case architecture + Case "AMD64" + GetPlatform = "x64" + Case "ARM64" + GetPlatform = "arm64" + Case "X86" + GetPlatform = "x86" + Case Else + GetPlatform = LCase(architecture) + End Select + End Function + + Dim filesystem + Dim shell + Dim locator + Dim service + Dim systems + Dim system + + Dim caption + Dim edition + Dim version + Dim build + Dim platform + Dim share + + Dim json + Dim temporary + Dim destination + Dim file + + Set filesystem = CreateObject("Scripting.FileSystemObject") + Set shell = CreateObject("WScript.Shell") + Set locator = CreateObject("WbemScripting.SWbemLocator") + Set service = locator.ConnectServer(".", "root\cimv2") + Set systems = service.ExecQuery("SELECT Caption, Version, BuildNumber FROM Win32_OperatingSystem") + + caption = "" + version = "" + build = "" + + For Each system In systems + caption = CStr(system.Caption) + version = CStr(system.Version) + build = CStr(system.BuildNumber) + Exit For + Next + + edition = ReadRegistry(shell, "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\EditionID") + platform = GetPlatform(shell) + + temporary = "\\host.lan\Data\windows.tmp" + destination = "\\host.lan\Data\windows.json" + + Do + On Error Resume Next + + share = ReadTextFile(filesystem, "\\host.lan\Data\readme.txt") + + If Err.Number = 0 Then + json = _ + "{" & _ + """token"":""" & EscapeJson(token) & """," & _ + """caption"":""" & EscapeJson(caption) & """," & _ + """edition"":""" & EscapeJson(edition) & """," & _ + """version"":""" & EscapeJson(version) & """," & _ + """build"":""" & EscapeJson(build) & """," & _ + """platform"":""" & EscapeJson(platform) & """," & _ + """share"":""" & EscapeJson(share) & """" & _ + "}" + + Set file = filesystem.OpenTextFile(temporary, ForWriting, True) + file.Write json + file.Close + + If filesystem.FileExists(destination) Then + filesystem.DeleteFile destination, True + End If + + filesystem.MoveFile temporary, destination + End If + + If Err.Number = 0 Then + On Error GoTo 0 + Exit Do + End If + + Err.Clear + On Error GoTo 0 + + WScript.Sleep 10000 + Loop + VBSCRIPT + + cat > "$RUNNER_TEMP/oem/install.bat" <> "$GITHUB_OUTPUT" + + - name: Pull image + shell: bash + run: | + set -Eeuo pipefail + + docker pull "$IMAGE" + + docker image inspect "$IMAGE" \ + --format 'Digest: {{index .RepoDigests 0}}' + + - name: Install and validate Windows + shell: bash + env: + CPU: ${{ inputs.cpu }} + EXPECTED_TOKEN: ${{ steps.test.outputs.token }} + EXPECTED_CAPTION: ${{ inputs.expected_caption }} + EXPECTED_EDITION: ${{ inputs.expected_edition }} + EXPECTED_PLATFORM: ${{ inputs.platform }} + MINIMUM_BUILD: ${{ inputs.minimum_build }} + VERSION: ${{ inputs.version }} + DISPLAY_NAME: ${{ inputs.name }} + run: | + set -Eeuo pipefail + + docker run --detach \ + --name "$CONTAINER" \ + --device /dev/kvm \ + --device /dev/net/tun \ + --cap-add NET_ADMIN \ + --stop-timeout 120 \ + --env "VERSION=$VERSION" \ + --env "RAM_SIZE=half" \ + --env "CPU_CORES=half" \ + --env "CPU_MODEL=$CPU" \ + --env "DISK_SIZE=64G" \ + --volume "$RUNNER_TEMP/data:/shared" \ + --volume "$RUNNER_TEMP/oem:/oem:ro" \ + --volume "$RUNNER_TEMP/storage:/storage" \ + "$IMAGE" + + echo + echo "Container log:" + echo "------------------------------------------------------------" + + docker logs \ + --follow \ + --timestamps \ + "$CONTAINER" & + + logs_pid="$!" + + stop_logs() { + kill "$logs_pid" 2>/dev/null || true + wait "$logs_pid" 2>/dev/null || true + } + + trap stop_logs EXIT + + deadline=$((SECONDS + 9000)) + boot_loop_limit=5 + + while (( SECONDS < deadline )); do + state="$( + docker inspect \ + --format '{{.State.Status}}' \ + "$CONTAINER" 2>/dev/null || true + )" + + if [[ "$state" != "running" ]]; then + echo + echo "------------------------------------------------------------" + echo "Container stopped before Windows became ready." + echo "Container state: ${state:-missing}" + exit 1 + fi + + container_log="$(docker logs "$CONTAINER" 2>&1 || true)" + + if grep -Eqi \ + 'KVM internal error|KVM: entry failed|hardware error 0x[0-9a-f]+|Triple fault' \ + <<< "$container_log"; then + echo + echo "------------------------------------------------------------" + echo "Detected a fatal QEMU or KVM error." + exit 1 + fi + + bios_starts="$( + grep -Fci 'SeaBIOS (version ' <<< "$container_log" || true + )" + + hard_disk_boots="$( + grep -Fci 'Booting from Hard Disk' <<< "$container_log" || true + )" + + dvd_boots="$( + grep -Fci 'Booting from DVD/CD' <<< "$container_log" || true + )" + + not_bootable_disk="$( + grep -Fci \ + 'Boot failed: not a bootable disk' \ + <<< "$container_log" || true + )" + + unreadable_boot_disk="$( + grep -Fci \ + 'Boot failed: could not read the boot disk' \ + <<< "$container_log" || true + )" + + no_bootable_device="$( + grep -Fci 'No bootable device.' <<< "$container_log" || true + )" + + bootmgr_missing="$( + grep -Fci 'BOOTMGR is missing' <<< "$container_log" || true + )" + + if (( bios_starts >= boot_loop_limit )) && + (( hard_disk_boots >= boot_loop_limit || + dvd_boots >= boot_loop_limit || + not_bootable_disk >= boot_loop_limit || + unreadable_boot_disk >= boot_loop_limit || + no_bootable_device >= boot_loop_limit || + bootmgr_missing >= boot_loop_limit )); then + echo + echo "------------------------------------------------------------" + echo "Detected a repeated BIOS boot loop." + echo "SeaBIOS starts: $bios_starts" + echo "Hard disk boots: $hard_disk_boots" + echo "DVD boots: $dvd_boots" + echo "Not-bootable disk failures: $not_bootable_disk" + echo "Unreadable boot-disk failures: $unreadable_boot_disk" + echo "No-bootable-device failures: $no_bootable_device" + echo "BOOTMGR failures: $bootmgr_missing" + exit 1 + fi + + response="" + + if [[ -s "$RUNNER_TEMP/data/windows.json" ]]; then + response="$(cat "$RUNNER_TEMP/data/windows.json")" + fi + + if [[ -n "$response" ]]; then + stop_logs + trap - EXIT + + echo + echo "------------------------------------------------------------" + echo "Received response:" + + if ! jq . <<< "$response"; then + echo "$response" + echo "The guest returned invalid JSON." + exit 1 + fi + + token="$(jq -r '.token // empty' <<< "$response")" + caption="$(jq -r '.caption // empty' <<< "$response")" + edition="$(jq -r '.edition // empty' <<< "$response")" + version="$(jq -r '.version // empty' <<< "$response")" + build="$(jq -r '.build // empty' <<< "$response")" + platform="$(jq -r '.platform // empty' <<< "$response")" + share="$(jq -r '.share // empty' <<< "$response")" + + if [[ "$token" != "$EXPECTED_TOKEN" ]]; then + echo "The response token does not match." + exit 1 + fi + + if [[ "$share" != "$EXPECTED_TOKEN" ]]; then + echo "Failed to read \\\\host.lan\\Data\\readme.txt." + echo "Expected contents:" + echo " $EXPECTED_TOKEN" + echo "Received:" + echo " ${share:-empty}" + exit 1 + fi + + normalized_caption="${caption//\(R\)/}" + normalized_expected_caption="${EXPECTED_CAPTION//\(R\)/}" + + if [[ "$normalized_caption" != *"$normalized_expected_caption"* ]]; then + echo "Expected caption containing:" + echo " $EXPECTED_CAPTION" + echo "Received:" + echo " $caption" + exit 1 + fi + + normalized_edition="${edition%Eval}" + normalized_expected_edition="${EXPECTED_EDITION%Eval}" + + if [[ -n "$EXPECTED_EDITION" && + "$normalized_edition" != "$normalized_expected_edition" ]]; then + echo "Expected edition: $EXPECTED_EDITION" + echo "Received edition: $edition" + exit 1 + fi + + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Unexpected Windows version: $version" + exit 1 + fi + + if [[ ! "$build" =~ ^[0-9]+$ ]]; then + echo "Invalid Windows build number: $build" + exit 1 + fi + + if (( build < MINIMUM_BUILD )); then + echo "Expected build $MINIMUM_BUILD or newer." + echo "Received build: $build" + exit 1 + fi + + if [[ "$platform" != "$EXPECTED_PLATFORM" ]]; then + echo "Expected platform:" + echo " $EXPECTED_PLATFORM" + echo "Received platform:" + echo " $platform" + exit 1 + fi + + echo + echo "$DISPLAY_NAME installed successfully." + echo "Version: $version" + echo "Build: $build" + echo "Platform: $platform" + echo "Shared file: accessible" + exit 0 + fi + + sleep 10 + done + + echo + echo "------------------------------------------------------------" + echo "Timed out waiting for Windows installation." + exit 1 + + - name: Cleanup + if: always() + shell: bash + run: | + docker rm --force "$CONTAINER" 2>/dev/null || true diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index 0abb64d7..f2669685 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -2,6 +2,8 @@ name: Links on: workflow_dispatch: + schedule: + - cron: '0 0 * * *' concurrency: group: links @@ -22,29 +24,63 @@ jobs: with: fetch-depth: 0 - - name: Validate Links + name: Validate all download mirrors + id: validate run: | errors=0 - count=0 + skipped=0 + tested=0 host="" + failed_links="" + failed_links_html="" declare -A seen + + ignored_domains=( + files.dog + bobpony.com + ) + wget https://github.com/lwthiker/curl-impersonate/releases/download/v0.6.1/curl-impersonate-v0.6.1.x86_64-linux-gnu.tar.gz tar -xzf curl-impersonate-v0.6.1.x86_64-linux-gnu.tar.gz sudo cp curl-impersonate-ff /usr/local/bin/ + check() { local url="$1" http + http=$(curl-impersonate-ff -sSL -o /dev/null -w "%{http_code}" --max-time 10 -I -- "$url" 2>&1) || http="000" [[ "$http" == 2* ]] && return 0 + http=$(curl-impersonate-ff -sSL -o /dev/null -w "%{http_code}" --max-time 10 -r "0-0" -- "$url" 2>&1) || http="000" [[ "$http" == 2* ]] } + + isIgnoredDomain() { + local url="$1" + local domain ignored + + domain="${url#*://}" + domain="${domain%%/*}" + domain="${domain%%:*}" + domain="${domain,,}" + + for ignored in "${ignored_domains[@]}"; do + if [[ "$domain" == "$ignored" || "$domain" == *."$ignored" ]]; then + return 0 + fi + done + + return 1 + } + while IFS= read -r line; do if [[ "$line" =~ ^[[:space:]]*local[[:space:]]+host=\"(https://[^\"]+)\" ]]; then host="${BASH_REMATCH[1]%/}" continue fi + [[ "$line" =~ ^[[:space:]]*url=\"(.+)\" ]] || continue val="${BASH_REMATCH[1]#/}" + if [[ "$val" == https://* ]]; then url="$val" elif [[ -n "$host" ]]; then @@ -52,16 +88,82 @@ jobs: else continue fi + [[ -v seen[$url] ]] && continue seen[$url]=1 - count=$((count + 1)) + + if isIgnoredDomain "$url"; then + echo "SKIP: $url (ignored domain)" + skipped=$((skipped + 1)) + continue + fi + + tested=$((tested + 1)) + if check "$url"; then echo " OK: $url" else echo "FAIL: $url" errors=$((errors + 1)) + failed_links+="$url"$'\n' + failed_links_html+="
  • $url
  • "$'\n' fi done < "src/define.sh" + echo "" - printf '%d/%d links valid\n' "$(( count - errors ))" "$count" - (( errors == 0 )) + printf '%d/%d tested links valid\n' "$((tested - errors))" "$tested" + + if (( skipped > 0 )); then + printf '%d link(s) skipped for ignored domains\n' "$skipped" + fi + + if (( errors > 0 )); then + printf '%d blocking failure(s)\n' "$errors" + + { + echo "failed=true" + echo "links<> "$GITHUB_OUTPUT" + else + echo "failed=false" >> "$GITHUB_OUTPUT" + fi + - + name: Send mail + if: steps.validate.outputs.failed == 'true' + uses: action-pack/send-mail@v1 + with: + to: ${{secrets.MAILTO}} + from: Github Actions <${{secrets.MAILTO}}> + connection_url: ${{secrets.MAIL_CONNECTION}} + subject: Mirror verification failed! + body: | + Mirror verification failed! + + The following links failed: + + ${{ steps.validate.outputs.links }} + + See https://github.com/${{ github.repository }}/actions for more information. + html_body: | +

    Mirror verification failed!

    + +

    The following links failed:

    + +
      + ${{ steps.validate.outputs.html_links }} +
    + +

    + See + GitHub Actions + for more information. +

    + - + name: Fail workflow + if: always() && steps.validate.outputs.failed == 'true' + run: exit 1 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 00000000..4f674164 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,249 @@ +name: Validation + +on: + release: + types: + - published + + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: validation-${{ github.ref }} + cancel-in-progress: true + +jobs: + desktop: + strategy: + fail-fast: false + matrix: + include: + - name: Windows 11 Pro + version: "11" + expected_caption: Windows 11 + expected_edition: Professional + minimum_build: 22000 + platform: x64 + + - name: Windows 11 LTSC + version: "11l" + expected_caption: Windows 11 + expected_edition: EnterpriseS + minimum_build: 26100 + platform: x64 + + - name: Windows 11 Enterprise + version: "11e" + expected_caption: Windows 11 + expected_edition: Enterprise + minimum_build: 22000 + platform: x64 + + - name: Windows 11 IoT Enterprise + version: "11i" + expected_caption: Windows 11 + expected_edition: IoTEnterpriseS + minimum_build: 26100 + platform: x64 + + - name: Windows 10 Pro + version: "10" + expected_caption: Windows 10 + expected_edition: Professional + minimum_build: 10240 + platform: x64 + + - name: Windows 10 LTSC + version: "10l" + expected_caption: Windows 10 + expected_edition: EnterpriseS + minimum_build: 17763 + platform: x64 + + - name: Windows 10 Enterprise + version: "10e" + expected_caption: Windows 10 + expected_edition: Enterprise + minimum_build: 10240 + platform: x64 + + - name: Windows 10 IoT Enterprise + version: "10i" + expected_caption: Windows 10 + expected_edition: IoTEnterpriseS + minimum_build: 19044 + platform: x64 + + - name: Windows 8.1 Enterprise + version: "8e" + expected_caption: Windows 8.1 + expected_edition: Enterprise + minimum_build: 9600 + platform: x64 + + name: ${{ matrix.name }} + uses: ./.github/workflows/install.yml + with: + name: ${{ matrix.name }} + version: ${{ matrix.version }} + expected_caption: ${{ matrix.expected_caption }} + expected_edition: ${{ matrix.expected_edition }} + minimum_build: ${{ matrix.minimum_build }} + platform: ${{ matrix.platform }} + + tiny: + strategy: + fail-fast: false + matrix: + include: + - name: Core11 + version: "core11" + expected_caption: Windows 11 + minimum_build: 22000 + platform: x64 + + - name: Tiny11 + version: "tiny11" + expected_caption: Windows 11 + minimum_build: 22000 + platform: x64 + + - name: Tiny10 + version: "tiny10" + expected_caption: Windows 10 + minimum_build: 19041 + platform: x64 + + name: ${{ matrix.name }} + uses: ./.github/workflows/install.yml + with: + name: ${{ matrix.name }} + version: ${{ matrix.version }} + expected_caption: ${{ matrix.expected_caption }} + minimum_build: ${{ matrix.minimum_build }} + platform: ${{ matrix.platform }} + + server: + strategy: + fail-fast: false + matrix: + include: + - name: Windows Server 2025 + version: "2025" + expected_caption: Windows Server 2025 + minimum_build: 26100 + platform: x64 + + - name: Windows Server 2022 + version: "2022" + expected_caption: Windows Server 2022 + minimum_build: 20348 + platform: x64 + + - name: Windows Server 2019 + version: "2019" + expected_caption: Windows Server 2019 + minimum_build: 17763 + platform: x64 + + - name: Windows Server 2016 + version: "2016" + expected_caption: Windows Server 2016 + minimum_build: 14393 + platform: x64 + + - name: Windows Server 2012 R2 + version: "2012" + expected_caption: Windows Server 2012 R2 + minimum_build: 9600 + platform: x64 + + name: ${{ matrix.name }} + uses: ./.github/workflows/install.yml + with: + name: ${{ matrix.name }} + version: ${{ matrix.version }} + expected_caption: ${{ matrix.expected_caption }} + minimum_build: ${{ matrix.minimum_build }} + platform: ${{ matrix.platform }} + + legacy: + strategy: + fail-fast: false + matrix: + include: + - name: Windows 7 Ultimate + version: "7u" + cpu: host + expected_caption: Windows 7 + minimum_build: 7601 + platform: x64 + + - name: Windows 7 Ultimate x86 + version: "7x86" + cpu: host + expected_caption: Windows 7 + minimum_build: 7601 + platform: x86 + + - name: Windows Vista Ultimate + version: "vu" + cpu: host + expected_caption: Windows Vista + minimum_build: 6002 + platform: x64 + + - name: Windows Vista Ultimate x86 + version: "vistax86" + cpu: host + expected_caption: Windows Vista + minimum_build: 6002 + platform: x86 + + - name: Windows XP Professional + version: "xp" + cpu: host + expected_caption: Windows XP + minimum_build: 2600 + platform: x86 + + - name: Windows XP Professional x64 + version: "xp64" + cpu: host + expected_caption: Windows XP + minimum_build: 3790 + platform: x64 + + - name: Windows 2000 Professional + version: "2k" + cpu: host + expected_caption: Windows 2000 + minimum_build: 2195 + platform: x86 + + - name: Windows Server 2008 R2 + version: "2008" + cpu: host + expected_caption: Windows Server 2008 R2 + minimum_build: 7601 + platform: x64 + + - name: Windows Server 2003 R2 + version: "2003" + cpu: host + expected_caption: Windows Server 2003 + minimum_build: 3790 + platform: x64 + + name: ${{ matrix.name }} + uses: ./.github/workflows/install.yml + with: + name: ${{ matrix.name }} + version: ${{ matrix.version }} + callback: legacy + cpu: ${{ matrix.cpu }} + expected_caption: ${{ matrix.expected_caption }} + minimum_build: ${{ matrix.minimum_build }} + platform: ${{ matrix.platform }}