1
0
mirror of https://github.com/dockur/windows.git synced 2026-07-27 21:42:36 +07:00
Files
windows/.github/workflows/install.yml
T

660 lines
19 KiB
YAML

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" <<EOF
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass ^
-File C:\OEM\ready.ps1 ^
-Token "$token"
EOF
;;
legacy)
cat > "$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" <<EOF
@echo off
cscript.exe //B //NoLogo C:\OEM\ready.vbs "$token"
EOF
;;
esac
echo "token=$token" >> "$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
if grep -Fqi \
'CDBOOT: Cannot boot from CD - Code: 5' \
<<< "$container_log"; then
echo
echo "------------------------------------------------------------"
echo "Detected an unbootable installation medium."
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