Build #783
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build | |
| on: | |
| workflow_dispatch: | |
| schedule: | |
| - cron: '0 0 * * *' | |
| concurrency: | |
| group: build | |
| cancel-in-progress: false | |
| jobs: | |
| build: | |
| name: Build | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: write | |
| packages: write | |
| contents: read | |
| steps: | |
| - | |
| name: Get latest version | |
| run: | | |
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| api="https://api.access.redhat.com/management/v1" | |
| arch="x86_64" | |
| limit=100 | |
| initial_offset=3500 | |
| resp=$( | |
| curl -fsS \ | |
| "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token" \ | |
| --data-urlencode "grant_type=refresh_token" \ | |
| --data-urlencode "client_id=rhsm-api" \ | |
| --data-urlencode "refresh_token=${{ secrets.REDHAT_TOKEN }}" | |
| ) | |
| token=$(jq -er '.access_token' <<< "$resp") | |
| version="" | |
| release="" | |
| checksum="" | |
| distro="" | |
| for major in {15..7}; do | |
| found="N" | |
| for variant in "-beta" "-eus" ""; do | |
| candidate="rhel-${major}-for-x86_64-appstream${variant}-rpms" | |
| valid="N" | |
| echo "Checking $candidate..." | |
| for start_offset in "$initial_offset" 0; do | |
| if [ "$start_offset" -eq 0 ]; then | |
| [ "$valid" = "Y" ] || break | |
| echo "Retrying $candidate from offset 0..." | |
| fi | |
| offset=$start_offset | |
| while :; do | |
| [[ "$offset" != "$initial_offset" ]] && echo "Offset $offset" | |
| resp=$( | |
| curl -sS \ | |
| -H "Authorization: Bearer $token" \ | |
| -H "Accept: application/json" \ | |
| "$api/packages/cset/$candidate/arch/$arch?limit=$limit&offset=$offset&filter=latest" | |
| ) | |
| if jq -e '.error' >/dev/null 2>&1 <<< "$resp"; then | |
| break | |
| fi | |
| if ! jq -e '.body | type == "array"' >/dev/null 2>&1 <<< "$resp"; then | |
| echo "Unexpected response for $candidate:" | |
| echo "$resp" | |
| break | |
| fi | |
| valid="Y" | |
| match=$( | |
| jq -c \ | |
| 'first(.body[]? | select(.name == "virtio-win")) // empty' \ | |
| <<< "$resp" | |
| ) | |
| if [ -n "$match" ]; then | |
| version=$(jq -er '.version' <<< "$match") | |
| release=$(jq -er '.release' <<< "$match") | |
| checksum=$(jq -er '.checksum' <<< "$match") | |
| distro=$candidate | |
| found="Y" | |
| break | |
| fi | |
| count=$(jq -r '.body | length' <<< "$resp") | |
| if [ "$count" -lt "$limit" ]; then | |
| break | |
| fi | |
| offset=$((offset + limit)) | |
| sleep 0.2 | |
| done | |
| [ "$found" = "Y" ] && break | |
| done | |
| [ "$found" = "Y" ] && break | |
| done | |
| [ "$found" = "Y" ] && break | |
| done | |
| if [ -z "$version" ] || [ -z "$release" ] || [ -z "$checksum" ]; then | |
| echo "ERROR: Could not find virtio-win in any RHEL AppStream content set." >&2 | |
| exit 1 | |
| fi | |
| pkg="$version-$release" | |
| latest_tag="${pkg%.el*}" | |
| if [ -z "$latest_tag" ]; then | |
| echo "ERROR: Tag not found." >&2 | |
| exit 1 | |
| fi | |
| echo "Selected content set: $distro" | |
| echo "Found tag: \"$latest_tag\"" | |
| resp=$( | |
| curl -fsS \ | |
| -H "Authorization: Bearer $token" \ | |
| -H "Accept: application/json" \ | |
| "$api/packages/$checksum/download" | |
| ) | |
| download=$(jq -er '.body.href' <<< "$resp") | |
| base=${latest_tag%%-*} | |
| { | |
| echo "latest_tag=$latest_tag" | |
| echo "latest_base=$base" | |
| echo "latest_download=$download" | |
| } >> "$GITHUB_ENV" | |
| - | |
| name: Check if the tag exists locally | |
| uses: action-pack/tag-exists@v1 | |
| id: checkTag | |
| with: | |
| tag: 'v${{ env.latest_tag }}' | |
| - | |
| name: Finish when found | |
| run: | | |
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| echo "exists=${{ steps.checkTag.outputs.exists }}" >> "$GITHUB_ENV" | |
| - | |
| name: Download package | |
| if: env.exists == 'false' | |
| id: rocky | |
| run: | | |
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| wget "${{ env.latest_download }}" -O /tmp/latest.rpm -q | |
| - | |
| name: Process package | |
| if: env.exists == 'false' | |
| id: process | |
| run: | | |
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| sudo apt-get update | |
| sudo apt-get install -y rdfind genisoimage libarchive-tools rpm unzip | |
| rpm -qp --changelog /tmp/latest.rpm > /tmp/rpm-changelog | |
| rm -rf /tmp/rpm /tmp/iso | |
| mkdir -p /tmp/rpm /tmp/iso | |
| cd /tmp/rpm | |
| rpm2cpio /tmp/latest.rpm | cpio -idm | |
| cp -L \ | |
| /tmp/rpm/usr/share/virtio-win/virtio-win.iso \ | |
| /tmp/latest.iso | |
| bsdtar -xf /tmp/latest.iso -C /tmp/iso | |
| - | |
| name: Strip unnecessary bloat | |
| if: env.exists == 'false' | |
| id: strip | |
| run: | | |
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| # Remove bloat. | |
| sudo rm -f /tmp/iso/winfsp-* | |
| # Remove tools for other platforms. | |
| sudo rm -rf \ | |
| /tmp/iso/i386 \ | |
| /tmp/iso/tools \ | |
| /tmp/iso/guest-agent | |
| sudo rm -f \ | |
| /tmp/iso/virtio-win-gt-x86.msi \ | |
| /tmp/iso/virtio-win-guest-tools.exe | |
| # Remove test and bridge utilities. | |
| if [ -d /tmp/iso/viosock ]; then | |
| sudo find /tmp/iso/viosock \ | |
| -type f \ | |
| \( \ | |
| -iname 'viosock-test.exe' -o \ | |
| -iname 'viosocklib-test.exe' -o \ | |
| -iname 'vstbridge.exe' \ | |
| \) \ | |
| -delete | |
| fi | |
| # Remove debug symbols. | |
| sudo find /tmp/iso -type f -iname '*.pdb' -delete | |
| # Remove modern drivers for other architectures. | |
| sudo find /tmp/iso \ | |
| -type d \ | |
| \( -iname x86 -o -iname arm64 \) \ | |
| -prune \ | |
| -exec rm -rf {} + | |
| # Remove empty directories. | |
| sudo find /tmp/iso -depth -type d -empty -delete | |
| - | |
| name: Add legacy drivers | |
| if: env.exists == 'false' | |
| id: legacy | |
| run: | | |
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| url="https://github.com/qemus/virtiso-whql/releases/download/v1.9.40-0/legacy.zip" | |
| wget "$url" -O /tmp/legacy.zip -q | |
| sudo unzip -o /tmp/legacy.zip -d /tmp/iso >/dev/null | |
| # Replace any symlinks from the archive with normal files or directories. | |
| sudo bash -Eeuo pipefail <<'EOF' | |
| while IFS= read -r -d '' link; do | |
| target=$(readlink -f -- "$link") | |
| if [ -z "$target" ] || [ ! -e "$target" ]; then | |
| echo "ERROR: Broken symlink in legacy archive: $link" >&2 | |
| exit 1 | |
| fi | |
| case "$target" in | |
| /tmp/iso/*) ;; | |
| *) | |
| echo "ERROR: Symlink points outside the ISO tree: $link -> $target" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| replacement="${link}.resolved" | |
| rm -rf -- "$replacement" | |
| if [ -d "$target" ]; then | |
| cp -aL -- "$target" "$replacement" | |
| else | |
| cp -L --preserve=mode,timestamps -- "$link" "$replacement" | |
| fi | |
| rm -rf -- "$link" | |
| mv -- "$replacement" "$link" | |
| done < <(find /tmp/iso -type l -print0) | |
| find /tmp/iso -depth -type d -empty -delete | |
| EOF | |
| - | |
| name: Generate release notes | |
| if: env.exists == 'false' | |
| id: notes | |
| env: | |
| LATEST_TAG: ${{ env.latest_tag }} | |
| run: | | |
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| python3 <<'PY' | |
| import json | |
| import os | |
| import re | |
| from collections import defaultdict | |
| from pathlib import Path | |
| iso_root = Path("/tmp/iso") | |
| info_file = iso_root / "data/info.json" | |
| changelog_file = Path("/tmp/rpm-changelog") | |
| output_file = Path("/tmp/release-notes.md") | |
| latest_tag = os.environ["LATEST_TAG"] | |
| if not info_file.is_file(): | |
| raise SystemExit( | |
| "ERROR: data/info.json was not found in the ISO." | |
| ) | |
| if not changelog_file.is_file(): | |
| raise SystemExit( | |
| "ERROR: RPM changelog was not generated." | |
| ) | |
| changelog = changelog_file.read_text( | |
| encoding="utf-8", | |
| errors="replace", | |
| ) | |
| changelog = changelog.replace("\r\n", "\n").replace("\r", "\n") | |
| entries = [ | |
| entry.strip() | |
| for entry in re.split( | |
| r"(?=^\*[ \t])", | |
| changelog, | |
| flags=re.MULTILINE, | |
| ) | |
| if entry.strip() | |
| ] | |
| selected_entry = "" | |
| for entry in entries: | |
| if re.search( | |
| rf"(?:^|[ \t-]){re.escape(latest_tag)}(?:\s|$)", | |
| entry, | |
| ): | |
| selected_entry = entry | |
| break | |
| if not selected_entry and entries: | |
| selected_entry = entries[0] | |
| selected_entry = re.sub( | |
| r"^\*[ \t]*", | |
| "", | |
| selected_entry, | |
| ).strip() | |
| header = "" | |
| details = [] | |
| if selected_entry: | |
| lines = selected_entry.splitlines() | |
| header = lines[0].strip() | |
| for line in lines[1:]: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| details.append( | |
| re.sub(r"^-[ \t]*", "", line) | |
| ) | |
| lines = ["## RPM changelog", ""] | |
| if header: | |
| lines.append(header) | |
| else: | |
| lines.append(latest_tag) | |
| if details: | |
| lines.append("") | |
| for detail in details: | |
| lines.append(f"- {detail}") | |
| windows_names = { | |
| "2k25": "Windows Server 2025", | |
| "w11": "Windows 11", | |
| "2k22": "Windows Server 2022", | |
| "2k19": "Windows Server 2019", | |
| "2k16": "Windows Server 2016", | |
| "w10": "Windows 10", | |
| "2k12R2": "Windows Server 2012 R2", | |
| "w8.1": "Windows 8.1", | |
| "2k12": "Windows Server 2012", | |
| "w8": "Windows 8", | |
| "2k8R2": "Windows Server 2008 R2", | |
| "w7": "Windows 7", | |
| "2k8": "Windows Server 2008", | |
| "2k3": "Windows Server 2003", | |
| "xp": "Windows XP", | |
| } | |
| preferred_order = list(windows_names) | |
| with info_file.open(encoding="utf-8") as file: | |
| data = json.load(file) | |
| grouped = defaultdict(lambda: defaultdict(set)) | |
| for driver in data.get("drivers", []): | |
| if str(driver.get("arch", "")).lower() != "amd64": | |
| continue | |
| inf_path = str( | |
| driver.get("inf_path", "") | |
| ).strip() | |
| if not inf_path: | |
| continue | |
| inf = Path(inf_path) | |
| if not inf.parts: | |
| continue | |
| component = inf.parts[0].lower() | |
| # Ignore the separate legacy QEMU FWCfg package. | |
| if component == "qemufwcfg": | |
| continue | |
| # Do not list drivers removed from the minified ISO. | |
| if not (iso_root / inf).is_file(): | |
| continue | |
| windows_version = str( | |
| driver.get("windows_version", "") | |
| ).strip() | |
| driver_version = str( | |
| driver.get("driver_version", "") | |
| ).strip() | |
| if not windows_version or not driver_version: | |
| continue | |
| # Remove the date preceding the actual driver version. | |
| if "," in driver_version: | |
| driver_version = driver_version.split( | |
| ",", | |
| 1, | |
| )[1].strip() | |
| component_name = str( | |
| driver.get("name", component) | |
| ).strip() | |
| if not component_name: | |
| component_name = component | |
| grouped[windows_version][driver_version].add( | |
| component_name | |
| ) | |
| windows_order = [ | |
| version | |
| for version in preferred_order | |
| if version in grouped | |
| ] | |
| windows_order.extend( | |
| sorted( | |
| set(grouped) - set(windows_order), | |
| key=str.casefold, | |
| ) | |
| ) | |
| if grouped: | |
| lines.extend(["", "## Driver versions"]) | |
| for windows_version in windows_order: | |
| versions = grouped[windows_version] | |
| heading = windows_names.get( | |
| windows_version, | |
| windows_version, | |
| ) | |
| lines.extend(["", f"### {heading}", ""]) | |
| for driver_version in sorted( | |
| versions, | |
| reverse=True, | |
| ): | |
| components = sorted( | |
| versions[driver_version], | |
| key=str.casefold, | |
| ) | |
| lines.append( | |
| f"- `{driver_version}` — " | |
| f"{', '.join(components)}" | |
| ) | |
| output_file.write_text( | |
| "\n".join(lines).rstrip() + "\n", | |
| encoding="utf-8", | |
| ) | |
| PY | |
| cat /tmp/release-notes.md | |
| delimiter="RELEASE_NOTES_$(date +%s%N)" | |
| { | |
| echo "release_notes<<$delimiter" | |
| cat /tmp/release-notes.md | |
| echo "$delimiter" | |
| } >> "$GITHUB_OUTPUT" | |
| - | |
| name: Deduplicate files | |
| if: env.exists == 'false' | |
| id: deduplicate | |
| run: | | |
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| # Reuse the Windows 8.1 viogpudo package for Windows 8. | |
| sudo cp -al \ | |
| /tmp/iso/viogpudo/w8.1 \ | |
| /tmp/iso/viogpudo/w8 | |
| # Add the required WDF installer DLL using hard links. | |
| wdf="/tmp/iso/vioserial/w7/amd64/WdfCoInstaller01009.dll" | |
| sudo cp -l "$wdf" /tmp/iso/Balloon/xp/amd64/WdfCoInstaller01009.dll | |
| sudo cp -l "$wdf" /tmp/iso/Balloon/2k3/amd64/WdfCoInstaller01009.dll | |
| sudo cp -l "$wdf" /tmp/iso/Balloon/2k8/amd64/WdfCoInstaller01009.dll | |
| sudo cp -l "$wdf" /tmp/iso/pvpanic/2k8/amd64/WdfCoInstaller01009.dll | |
| sudo cp -l "$wdf" /tmp/iso/vioserial/xp/amd64/WdfCoInstaller01009.dll | |
| sudo cp -l "$wdf" /tmp/iso/vioserial/2k3/amd64/WdfCoInstaller01009.dll | |
| sudo cp -l "$wdf" /tmp/iso/vioserial/2k8/amd64/WdfCoInstaller01009.dll | |
| # Replace all other byte-identical files with hard links. | |
| sudo rdfind \ | |
| -outputname /dev/null \ | |
| -removeidentinode false \ | |
| -makehardlinks true \ | |
| /tmp/iso | |
| - | |
| name: Verify driver folders | |
| if: env.exists == 'false' | |
| id: verify | |
| run: | | |
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| missing="/tmp/missing-driver-folders.txt" | |
| : > "$missing" | |
| while IFS=: read -r driver versions; do | |
| [ -n "$driver" ] || continue | |
| for version in $versions; do | |
| path="/tmp/iso/$driver/$version" | |
| if [ ! -d "$path" ]; then | |
| echo "$driver/$version" >> "$missing" | |
| fi | |
| done | |
| done <<'EOF' | |
| Balloon:2k3 xp 2k8 2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25 | |
| NetKVM:2k3 xp 2k8 2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25 | |
| fwcfg:2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25 | |
| pvpanic:2k8 2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25 | |
| qemufwcfg:2k16 2k19 w10 w11 2k22 2k25 | |
| qemupciserial:2k8 2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25 | |
| sata:xp | |
| smbus:2k8 | |
| sriov:w8.1 2k12R2 2k16 2k19 w10 w11 2k22 2k25 | |
| viofs:w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25 | |
| viogpudo:w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25 | |
| vioinput:2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25 | |
| viomem:2k16 2k19 w10 w11 2k22 2k25 | |
| viorng:2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25 | |
| vioscsi:2k8 2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25 | |
| vioserial:2k3 xp 2k8 2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25 | |
| viosock:2k16 2k19 w10 w11 2k22 2k25 | |
| viostor:2k3 xp 2k8 2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25 | |
| EOF | |
| if [ -s "$missing" ]; then | |
| echo "ERROR: Previously available driver folders are missing:" >&2 | |
| sed 's/^/ - /' "$missing" >&2 | |
| echo >&2 | |
| echo "The upstream package may have dropped legacy Windows drivers." >&2 | |
| echo "Add the missing drivers to legacy.zip before publishing this release." >&2 | |
| exit 1 | |
| fi | |
| echo "All expected driver folders are present." | |
| - | |
| name: Build minified ISO | |
| if: env.exists == 'false' | |
| id: build | |
| run: | | |
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| base="${{ env.latest_base }}" | |
| owner="$(id -u):$(id -g)" | |
| sudo genisoimage \ | |
| -o "/tmp/virtio-win-${base}.iso" \ | |
| -r \ | |
| -iso-level 4 \ | |
| -input-charset iso8859-1 \ | |
| -cache-inodes \ | |
| -V "virtio-win-${base}" \ | |
| /tmp/iso | |
| # Add a stable filename for the latest-release download URL. | |
| cp \ | |
| "/tmp/virtio-win-${base}.iso" \ | |
| /tmp/virtio-win.iso | |
| # Keep the MSI files in the ISO, but omit them from the archives. | |
| sudo rm -f /tmp/iso/*.msi | |
| sudo tar \ | |
| --owner=0 \ | |
| --group=0 \ | |
| --numeric-owner \ | |
| -C /tmp/iso \ | |
| -czf "/tmp/virtio-win-${base}.tar.gz" \ | |
| . | |
| sudo env XZ_OPT=-9 tar \ | |
| --owner=0 \ | |
| --group=0 \ | |
| --numeric-owner \ | |
| -C /tmp/iso \ | |
| -cJf "/tmp/virtio-win-${base}.tar.xz" \ | |
| . | |
| sudo chown "$owner" \ | |
| "/tmp/virtio-win-${base}.iso" \ | |
| "/tmp/virtio-win-${base}.tar.gz" \ | |
| "/tmp/virtio-win-${base}.tar.xz" | |
| chown "$owner" /tmp/virtio-win.iso | |
| - | |
| name: Checkout | |
| if: env.exists == 'false' | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - | |
| name: Create a release | |
| if: env.exists == 'false' | |
| uses: action-pack/github-release@v2 | |
| with: | |
| body: ${{ steps.notes.outputs.release_notes }} | |
| tag: "v${{ env.latest_tag }}" | |
| title: "v${{ env.latest_base }}" | |
| token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| - | |
| name: Update release | |
| if: env.exists == 'false' | |
| uses: AButler/upload-release-assets@v4.0 | |
| with: | |
| files: '/tmp/virtio-win*' | |
| release-tag: "v${{ env.latest_tag }}" | |
| repo-token: ${{ secrets.REPO_ACCESS_TOKEN }} | |
| - | |
| name: Send mail | |
| if: env.exists == 'false' | |
| uses: action-pack/send-mail@v1 | |
| with: | |
| to: ${{ secrets.MAILTO }} | |
| from: Github Actions <${{ secrets.MAILTO }}> | |
| connection_url: ${{ secrets.MAIL_CONNECTION }} | |
| subject: Build of ${{ github.event.repository.name }} v${{ env.latest_tag }} completed | |
| body: | | |
| The build job of ${{ github.event.repository.name }} v${{ env.latest_tag }} was completed successfully! | |
| See https://github.com/${{ github.repository }}/actions for more information. |