Skip to content

Commit 12c35c9

Browse files
committed
add GitHub Actions workflow for building Windows qcow2
Self-hosted runner with KVM. Workflow: 1. Download Windows ISO (from WINDOWS_ISO_URL secret) + virtio-win ISO 2. QEMU unattended install with autounattend.xml via floppy 3. Poll SSH for C:\install.success marker to detect completion 4. Compress qcow2, split into sub-2GiB parts 5. Upload to GitHub Releases with checksums and manifest Triggered via workflow_dispatch with configurable version tag and disk size.
1 parent 275d678 commit 12c35c9

1 file changed

Lines changed: 195 additions & 0 deletions

File tree

.github/workflows/build.yml

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
name: Build Windows qcow2
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
windows_version:
7+
description: "Windows version tag (used in release name)"
8+
required: true
9+
default: "win11-25h2"
10+
disk_size:
11+
description: "Disk image size"
12+
required: true
13+
default: "40G"
14+
15+
env:
16+
VIRTIO_WIN_URL: "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.285-1/virtio-win-0.1.285.iso"
17+
SSH_PORT: 2222
18+
QCOW2_NAME: "windows-11-25h2.qcow2"
19+
20+
jobs:
21+
build:
22+
runs-on: self-hosted
23+
timeout-minutes: 180
24+
steps:
25+
- uses: actions/checkout@v4
26+
27+
- name: Verify KVM
28+
run: |
29+
test -e /dev/kvm || { echo "::error::KVM not available"; exit 1; }
30+
ls -la /dev/kvm
31+
32+
- name: Install dependencies
33+
run: |
34+
sudo apt-get update
35+
sudo apt-get install -y --no-install-recommends \
36+
qemu-system-x86 qemu-utils \
37+
ovmf swtpm mtools \
38+
openssh-client sshpass jq
39+
40+
- name: Download virtio-win ISO
41+
run: curl -fsSL -o virtio-win.iso "$VIRTIO_WIN_URL"
42+
43+
- name: Download Windows ISO
44+
run: |
45+
test -n "${{ secrets.WINDOWS_ISO_URL }}" || { echo "::error::WINDOWS_ISO_URL secret not set"; exit 1; }
46+
curl -fsSL -o windows.iso "${{ secrets.WINDOWS_ISO_URL }}"
47+
48+
- name: Create disk image
49+
run: qemu-img create -f qcow2 "$QCOW2_NAME" "${{ inputs.disk_size }}"
50+
51+
- name: Prepare OVMF variables
52+
run: cp /usr/share/OVMF/OVMF_VARS_4M.fd OVMF_VARS.fd
53+
54+
- name: Start TPM emulator
55+
run: |
56+
mkdir -p /tmp/mytpm
57+
swtpm socket --tpmstate dir=/tmp/mytpm \
58+
--ctrl type=unixio,path=/tmp/swtpm-sock \
59+
--tpm2 --log level=5 &
60+
sleep 1
61+
test -S /tmp/swtpm-sock
62+
63+
- name: Create autounattend floppy
64+
run: |
65+
mkfs.fat -C autounattend.img 1440
66+
mcopy -i autounattend.img autounattend.xml ::/autounattend.xml
67+
68+
- name: Install Windows
69+
run: |
70+
qemu-system-x86_64 \
71+
-machine q35,accel=kvm,smm=on \
72+
-cpu host,hv_relaxed,hv_spinlocks=0x1fff,hv_vapic,hv_time \
73+
-m 8G -smp 4 \
74+
-global driver=cfi.pflash01,property=secure,value=on \
75+
-drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.secboot.fd \
76+
-drive if=pflash,format=raw,file=OVMF_VARS.fd \
77+
-cdrom windows.iso \
78+
-drive file=virtio-win.iso,index=1,media=cdrom \
79+
-drive if=none,id=root,file="$QCOW2_NAME",format=qcow2 \
80+
-device virtio-blk-pci,drive=root,disable-legacy=on \
81+
-device virtio-net-pci,netdev=mynet0,disable-legacy=on \
82+
-netdev user,id=mynet0,hostfwd=tcp::${SSH_PORT}-:22 \
83+
-chardev socket,id=chrtpm,path=/tmp/swtpm-sock \
84+
-tpmdev emulator,id=tpm0,chardev=chrtpm \
85+
-device tpm-tis,tpmdev=tpm0 \
86+
-vga none -nographic \
87+
-drive file=autounattend.img,format=raw,if=floppy \
88+
-serial file:serial.log \
89+
-daemonize -pidfile qemu.pid
90+
echo "QEMU started, PID=$(cat qemu.pid)"
91+
92+
- name: Wait for installation to complete
93+
run: |
94+
# Wait for SSH to become available and install.success to appear.
95+
# Windows install typically takes 30-90 minutes.
96+
MAX_WAIT=7200 # 2 hours
97+
INTERVAL=60
98+
ELAPSED=0
99+
100+
echo "Waiting for Windows installation to complete..."
101+
while [ $ELAPSED -lt $MAX_WAIT ]; do
102+
sleep $INTERVAL
103+
ELAPSED=$((ELAPSED + INTERVAL))
104+
echo "[${ELAPSED}s] Checking SSH on port ${SSH_PORT}..."
105+
106+
# Try SSH: check if install.success exists
107+
if sshpass -p 'C@c#on160' ssh -o StrictHostKeyChecking=no \
108+
-o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null \
109+
-p "$SSH_PORT" cocoon@localhost \
110+
"if exist C:\\install.success echo READY" 2>/dev/null | grep -q READY; then
111+
echo "install.success detected at ${ELAPSED}s"
112+
exit 0
113+
fi
114+
done
115+
116+
echo "::error::Timed out waiting for installation after ${MAX_WAIT}s"
117+
exit 1
118+
119+
- name: Shut down VM
120+
run: |
121+
sshpass -p 'C@c#on160' ssh -o StrictHostKeyChecking=no \
122+
-o UserKnownHostsFile=/dev/null \
123+
-p "$SSH_PORT" cocoon@localhost \
124+
"shutdown /s /t 10" 2>/dev/null || true
125+
126+
# Wait for QEMU process to exit
127+
QEMU_PID=$(cat qemu.pid)
128+
for i in $(seq 1 60); do
129+
kill -0 "$QEMU_PID" 2>/dev/null || { echo "VM shut down after ${i}s"; break; }
130+
sleep 1
131+
done
132+
133+
# Force kill if still running
134+
kill -0 "$QEMU_PID" 2>/dev/null && { echo "Force killing QEMU"; kill -9 "$QEMU_PID"; sleep 2; }
135+
136+
- name: Compress qcow2
137+
run: |
138+
echo "Original size: $(du -sh "$QCOW2_NAME" | cut -f1)"
139+
qemu-img convert -O qcow2 -c "$QCOW2_NAME" "${QCOW2_NAME%.qcow2}-compressed.qcow2"
140+
mv "${QCOW2_NAME%.qcow2}-compressed.qcow2" "$QCOW2_NAME"
141+
echo "Compressed size: $(du -sh "$QCOW2_NAME" | cut -f1)"
142+
143+
- name: Split image and generate checksums
144+
run: |
145+
# Split into sub-2GiB parts for GitHub Releases
146+
split -b 1900M -d --additional-suffix=.part "$QCOW2_NAME" "${QCOW2_NAME}."
147+
rm "$QCOW2_NAME"
148+
149+
# Generate checksums
150+
sha256sum *.part > SHA256SUMS
151+
cat SHA256SUMS
152+
153+
# Generate manifest
154+
cat > manifest.json <<MANIFEST
155+
{
156+
"version": "${{ inputs.windows_version }}",
157+
"created": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
158+
"original_name": "$QCOW2_NAME",
159+
"disk_size": "${{ inputs.disk_size }}",
160+
"parts": $(ls -1 *.part | jq -R . | jq -s .),
161+
"reassemble": "cat ${QCOW2_NAME}.*.part > ${QCOW2_NAME}"
162+
}
163+
MANIFEST
164+
165+
- name: Create GitHub Release
166+
env:
167+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
168+
run: |
169+
TAG="${{ inputs.windows_version }}-$(date +%Y%m%d)"
170+
gh release create "$TAG" \
171+
--title "Windows Image ${{ inputs.windows_version }}" \
172+
--notes "$(cat <<'NOTES'
173+
## Windows 11 25H2 qcow2 image
174+
175+
### Reassemble
176+
```bash
177+
# Download all .part files, then:
178+
cat windows-11-25h2.qcow2.*.part > windows-11-25h2.qcow2
179+
sha256sum -c SHA256SUMS
180+
```
181+
182+
### Verify
183+
```bash
184+
qemu-img info windows-11-25h2.qcow2
185+
```
186+
NOTES
187+
)" \
188+
*.part SHA256SUMS manifest.json
189+
190+
- name: Cleanup
191+
if: always()
192+
run: |
193+
kill $(cat qemu.pid 2>/dev/null) 2>/dev/null || true
194+
sudo rm -rf /tmp/mytpm
195+
rm -f windows.iso virtio-win.iso OVMF_VARS.fd autounattend.img qemu.pid

0 commit comments

Comments
 (0)