Manual Releases #7
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: Manual Releases | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| bump_mode: | |
| description: Release bump policy | |
| required: true | |
| default: auto | |
| type: choice | |
| options: | |
| - auto | |
| - serial | |
| - minor | |
| version_override: | |
| description: Optional explicit release version (for example 0.1.011) | |
| required: false | |
| default: "" | |
| type: string | |
| concurrency: | |
| group: openq4-manual-releases | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| jobs: | |
| metadata: | |
| name: Release Metadata | |
| runs-on: ubuntu-latest | |
| outputs: | |
| current_version: ${{ steps.version.outputs.current_version }} | |
| latest_release_tag: ${{ steps.version.outputs.latest_release_tag }} | |
| latest_release_version: ${{ steps.version.outputs.latest_release_version }} | |
| version: ${{ steps.version.outputs.version }} | |
| version_tag: ${{ steps.version.outputs.version_tag }} | |
| release_tag: ${{ steps.version.outputs.release_tag }} | |
| release_name: ${{ steps.version.outputs.release_name }} | |
| release_scale: ${{ steps.version.outputs.release_scale }} | |
| release_reason: ${{ steps.version.outputs.release_reason }} | |
| bump_mode: ${{ steps.version.outputs.bump_mode }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Fetch tags | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git fetch --force --tags origin | |
| - name: Setup Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.x" | |
| - name: Compute release version | |
| id: version | |
| env: | |
| INPUT_BUMP_MODE: ${{ inputs.bump_mode }} | |
| INPUT_VERSION_OVERRIDE: ${{ inputs.version_override }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| args=( | |
| --source-root . | |
| --bump-mode "${INPUT_BUMP_MODE}" | |
| ) | |
| if [ -n "${INPUT_VERSION_OVERRIDE}" ]; then | |
| args+=(--version-override "${INPUT_VERSION_OVERRIDE}") | |
| fi | |
| python tools/build/openq4_release_version.py "${args[@]}" >> "$GITHUB_OUTPUT" | |
| builds: | |
| name: ${{ matrix.label }} | |
| needs: metadata | |
| runs-on: ${{ matrix.os }} | |
| env: | |
| OPENQ4_GAMELIBS_REPO: ${{ github.workspace }}/OpenQ4-GameLibs | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: windows-latest | |
| platform: windows | |
| label: Windows x64 | |
| binary_arch: x64 | |
| platform_backend: sdl3 | |
| archive_format: zip | |
| archive_ext: .zip | |
| - os: windows-11-arm | |
| platform: windows | |
| label: Windows ARM64 | |
| binary_arch: arm64 | |
| platform_backend: sdl3 | |
| archive_format: zip | |
| archive_ext: .zip | |
| - os: ubuntu-latest | |
| platform: linux | |
| label: Linux x64 | |
| binary_arch: x64 | |
| platform_backend: sdl3 | |
| archive_format: tar.xz | |
| archive_ext: .tar.xz | |
| - os: ubuntu-24.04-arm | |
| platform: linux | |
| label: Linux ARM64 | |
| binary_arch: arm64 | |
| platform_backend: sdl3 | |
| archive_format: tar.xz | |
| archive_ext: .tar.xz | |
| - os: macos-latest | |
| platform: macos | |
| label: macOS ARM64 | |
| binary_arch: arm64 | |
| platform_backend: sdl3 | |
| archive_format: tar.gz | |
| archive_ext: .tar.gz | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Fetch OpenQ4-GameLibs | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git clone --depth 1 https://github.com/themuffinator/OpenQ4-GameLibs.git OpenQ4-GameLibs | |
| - name: Setup Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.x" | |
| - name: Install Meson and Ninja | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install meson ninja markdown | |
| - name: Show release metadata | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| echo "Configured repo version: ${{ needs.metadata.outputs.current_version }}" | |
| echo "Latest release tag: ${{ needs.metadata.outputs.latest_release_tag }}" | |
| echo "Release version: ${{ needs.metadata.outputs.version }}" | |
| echo "Release tag: ${{ needs.metadata.outputs.release_tag }}" | |
| echo "Release scale: ${{ needs.metadata.outputs.release_scale }}" | |
| echo "Release reason: ${{ needs.metadata.outputs.release_reason }}" | |
| - name: Install Linux native dependencies | |
| if: matrix.platform == 'linux' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| for attempt in 1 2 3; do | |
| if sudo apt-get update && sudo apt-get install -y \ | |
| libasound2-dev \ | |
| libdbus-1-dev \ | |
| libdecor-0-dev \ | |
| libdrm-dev \ | |
| libegl1-mesa-dev \ | |
| libfribidi-dev \ | |
| libgbm-dev \ | |
| libgl1-mesa-dev \ | |
| libibus-1.0-dev \ | |
| libjack-dev \ | |
| libopenal-dev \ | |
| libpipewire-0.3-dev \ | |
| libpulse-dev \ | |
| libsndio-dev \ | |
| libthai-dev \ | |
| libudev-dev \ | |
| libwayland-dev \ | |
| libx11-dev \ | |
| libxcursor-dev \ | |
| libxext-dev \ | |
| libxfixes-dev \ | |
| libxi-dev \ | |
| libxkbcommon-dev \ | |
| libxrandr-dev \ | |
| libxss-dev \ | |
| libxtst-dev \ | |
| libxxf86dga-dev \ | |
| libxxf86vm-dev; then | |
| exit 0 | |
| fi | |
| if [ "${attempt}" -eq 3 ]; then | |
| echo "Failed to install Linux native dependencies after ${attempt} attempts." | |
| exit 1 | |
| fi | |
| echo "Linux dependency installation attempt ${attempt} failed; retrying in 10 seconds..." | |
| sleep 10 | |
| done | |
| - name: Install macOS native dependencies | |
| if: matrix.platform == 'macos' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| echo "Using bundled Meson fallbacks for macOS third-party libraries." | |
| - name: Prepare OpenAL Soft (Windows) | |
| if: matrix.platform == 'windows' | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $outputRoot = Join-Path $env:GITHUB_WORKSPACE ".tmp/openal-soft-${{ matrix.binary_arch }}" | |
| $env:OPENQ4_VS_TARGET_ARCH = "${{ matrix.binary_arch }}" | |
| powershell -ExecutionPolicy Bypass -File tools/build/prepare_windows_openal.ps1 -Architecture "${{ matrix.binary_arch }}" -OutputRoot $outputRoot | |
| "OPENQ4_OPENAL_ROOT=$outputRoot" >> $env:GITHUB_ENV | |
| - name: Build and install (Windows) | |
| if: matrix.platform == 'windows' | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $env:OPENQ4_SKIP_GAMELIBS_SYNC = "1" | |
| $env:OPENQ4_VS_TARGET_ARCH = "${{ matrix.binary_arch }}" | |
| $setupArgs = @( | |
| "setup", | |
| "--wipe", | |
| "builddir", | |
| ".", | |
| "--backend", | |
| "ninja", | |
| "--buildtype=release", | |
| "--wrap-mode=forcefallback", | |
| "-Dplatform_backend=${{ matrix.platform_backend }}", | |
| "-Dversion_track=stable", | |
| "-Dversion_base_override=${{ needs.metadata.outputs.version }}" | |
| ) | |
| if (-not [string]::IsNullOrWhiteSpace($env:OPENQ4_OPENAL_ROOT)) { | |
| $setupArgs += "-Dopenal_root_override=$env:OPENQ4_OPENAL_ROOT" | |
| } | |
| powershell -ExecutionPolicy Bypass -File tools/build/meson_setup.ps1 @setupArgs | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| powershell -ExecutionPolicy Bypass -File tools/build/meson_setup.ps1 compile -C builddir | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| powershell -ExecutionPolicy Bypass -File tools/build/meson_setup.ps1 install -C builddir --no-rebuild --skip-subprojects | |
| if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } | |
| - name: Build and install (Linux/macOS) | |
| if: matrix.platform != 'windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| export OPENQ4_SKIP_GAMELIBS_SYNC=1 | |
| bash tools/build/meson_setup.sh setup --wipe builddir . --backend ninja --buildtype=release --wrap-mode=forcefallback -Dplatform_backend=${{ matrix.platform_backend }} -Dversion_track=stable -Dversion_base_override=${{ needs.metadata.outputs.version }} | |
| bash tools/build/meson_setup.sh compile -C builddir | |
| bash tools/build/meson_setup.sh install -C builddir --no-rebuild --skip-subprojects | |
| - name: Validate staged payload | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ ! -d ".install/baseoq4" ]; then | |
| echo "Staged baseoq4 payload directory is missing." | |
| exit 1 | |
| fi | |
| has_payload_files="$(find ".install/baseoq4" -type f -print -quit 2>/dev/null || true)" | |
| if [ -z "${has_payload_files}" ]; then | |
| echo "Staged baseoq4 payload directory has no files." | |
| exit 1 | |
| fi | |
| if [ "${{ matrix.platform }}" = "linux" ]; then | |
| if [ ! -f ".install/share/applications/openq4.desktop" ]; then | |
| echo "Missing Linux desktop entry in staged payload." | |
| exit 1 | |
| fi | |
| if [ ! -f ".install/share/icons/hicolor/256x256/apps/openq4.png" ]; then | |
| echo "Missing Linux 256x256 icon in staged payload." | |
| exit 1 | |
| fi | |
| fi | |
| - name: Prepare package | |
| id: package | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python tools/build/package_release.py \ | |
| --platform "${{ matrix.platform }}" \ | |
| --arch "${{ matrix.binary_arch }}" \ | |
| --version "${{ needs.metadata.outputs.version }}" \ | |
| --version-tag "${{ needs.metadata.outputs.version_tag }}" \ | |
| --archive-format "${{ matrix.archive_format }}" \ | |
| --source-root "${GITHUB_WORKSPACE}" \ | |
| --output-dir "${RUNNER_TEMP}" | |
| archive_path="${RUNNER_TEMP}/openq4-${{ needs.metadata.outputs.version_tag }}-${{ matrix.platform }}-${{ matrix.binary_arch }}${{ matrix.archive_ext }}" | |
| package_dir="${RUNNER_TEMP}/openq4-${{ needs.metadata.outputs.version_tag }}-${{ matrix.platform }}-${{ matrix.binary_arch }}" | |
| if [ ! -f "${archive_path}" ]; then | |
| echo "Expected package archive not found: ${archive_path}" | |
| exit 1 | |
| fi | |
| if [ ! -d "${package_dir}" ]; then | |
| echo "Expected package directory not found: ${package_dir}" | |
| exit 1 | |
| fi | |
| echo "archive_path=${archive_path}" >> "$GITHUB_OUTPUT" | |
| echo "package_dir=${package_dir}" >> "$GITHUB_OUTPUT" | |
| - name: Install Inno Setup | |
| if: matrix.platform == 'windows' | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { | |
| throw "Chocolatey is not available on this Windows runner, so Inno Setup could not be installed." | |
| } | |
| choco install innosetup --no-progress -y | |
| - name: Build Windows installer | |
| if: matrix.platform == 'windows' | |
| id: windows_installer | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| python tools/build/build_windows_installer.py ` | |
| --package-dir "${{ steps.package.outputs.package_dir }}" ` | |
| --version "${{ needs.metadata.outputs.version }}" ` | |
| --version-tag "${{ needs.metadata.outputs.version_tag }}" ` | |
| --arch "${{ matrix.binary_arch }}" ` | |
| --source-root "${env:GITHUB_WORKSPACE}" ` | |
| --output-dir "${env:RUNNER_TEMP}" | |
| $installerPath = Join-Path $env:RUNNER_TEMP "openq4-${{ needs.metadata.outputs.version_tag }}-windows-${{ matrix.binary_arch }}-setup.exe" | |
| if (-not (Test-Path -LiteralPath $installerPath)) { | |
| throw "Expected Windows installer not found: $installerPath" | |
| } | |
| "installer_path=$installerPath" >> $env:GITHUB_OUTPUT | |
| - name: Validate macOS app bundle | |
| if: matrix.platform == 'macos' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| app_root="${{ steps.package.outputs.package_dir }}/OpenQ4.app" | |
| app_plist="${app_root}/Contents/Info.plist" | |
| app_exec="${app_root}/Contents/MacOS/OpenQ4" | |
| app_icon="${app_root}/Contents/Resources/OpenQ4.icns" | |
| if [ ! -f "${app_plist}" ]; then | |
| echo "Missing macOS app Info.plist: ${app_plist}" | |
| exit 1 | |
| fi | |
| if [ ! -x "${app_exec}" ]; then | |
| echo "Missing or non-executable macOS app launcher: ${app_exec}" | |
| exit 1 | |
| fi | |
| if [ ! -f "${app_icon}" ]; then | |
| echo "Missing macOS app icon: ${app_icon}" | |
| exit 1 | |
| fi | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: openq4-release-${{ needs.metadata.outputs.version_tag }}-${{ matrix.platform }}-${{ matrix.binary_arch }} | |
| path: ${{ steps.package.outputs.archive_path }} | |
| if-no-files-found: error | |
| - name: Upload Windows installer | |
| if: matrix.platform == 'windows' | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: openq4-release-${{ needs.metadata.outputs.version_tag }}-windows-${{ matrix.binary_arch }}-installer | |
| path: ${{ steps.windows_installer.outputs.installer_path }} | |
| if-no-files-found: error | |
| release: | |
| name: Publish Release | |
| needs: [metadata, builds] | |
| if: ${{ needs.builds.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Fetch tags | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git fetch --force --tags origin | |
| - name: Setup Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.x" | |
| - name: Download packaged artifacts | |
| uses: actions/download-artifact@v5 | |
| with: | |
| pattern: openq4-release-${{ needs.metadata.outputs.version_tag }}-* | |
| path: ${{ runner.temp }}/release-artifacts | |
| merge-multiple: true | |
| - name: Verify platform archives | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| artifact_dir="${RUNNER_TEMP}/release-artifacts" | |
| if [ ! -d "${artifact_dir}" ]; then | |
| echo "Artifact directory is missing: ${artifact_dir}" | |
| exit 1 | |
| fi | |
| ls -lah "${artifact_dir}" | |
| expected=( | |
| "openq4-${{ needs.metadata.outputs.version_tag }}-windows-x64.zip" | |
| "openq4-${{ needs.metadata.outputs.version_tag }}-windows-x64-setup.exe" | |
| "openq4-${{ needs.metadata.outputs.version_tag }}-windows-arm64.zip" | |
| "openq4-${{ needs.metadata.outputs.version_tag }}-windows-arm64-setup.exe" | |
| "openq4-${{ needs.metadata.outputs.version_tag }}-linux-x64.tar.xz" | |
| "openq4-${{ needs.metadata.outputs.version_tag }}-linux-arm64.tar.xz" | |
| "openq4-${{ needs.metadata.outputs.version_tag }}-macos-arm64.tar.gz" | |
| ) | |
| missing=0 | |
| for package_file in "${expected[@]}"; do | |
| if [ ! -f "${artifact_dir}/${package_file}" ]; then | |
| echo "Missing expected package archive: ${package_file}" | |
| missing=1 | |
| fi | |
| done | |
| if [ "${missing}" -ne 0 ]; then | |
| exit 1 | |
| fi | |
| - name: Generate changelog | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python tools/build/generate_release_changelog.py \ | |
| --version "${{ needs.metadata.outputs.version }}" \ | |
| --version-tag "${{ needs.metadata.outputs.version_tag }}" \ | |
| --release-tag "${{ needs.metadata.outputs.release_tag }}" \ | |
| --release-scale "${{ needs.metadata.outputs.release_scale }}" \ | |
| --release-reason "${{ needs.metadata.outputs.release_reason }}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --source-root "${GITHUB_WORKSPACE}" \ | |
| --run-id "${GITHUB_RUN_ID}" \ | |
| --run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ | |
| --output "${RUNNER_TEMP}/release-changelog.md" | |
| - name: Create or update release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| tag="${{ needs.metadata.outputs.release_tag }}" | |
| name="${{ needs.metadata.outputs.release_name }}" | |
| notes="${RUNNER_TEMP}/release-changelog.md" | |
| assets_dir="${RUNNER_TEMP}/release-artifacts" | |
| mapfile -t assets < <(find "${assets_dir}" -maxdepth 1 -type f | sort) | |
| if [ "${#assets[@]}" -eq 0 ]; then | |
| echo "No release assets were downloaded." | |
| exit 1 | |
| fi | |
| if gh release view "${tag}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then | |
| gh release edit "${tag}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --title "${name}" \ | |
| --notes-file "${notes}" | |
| gh release upload "${tag}" "${assets[@]}" --repo "${GITHUB_REPOSITORY}" --clobber | |
| else | |
| gh release create "${tag}" "${assets[@]}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --title "${name}" \ | |
| --notes-file "${notes}" | |
| fi | |
| release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}" --jq .id)" | |
| gh api \ | |
| --method PATCH \ | |
| "repos/${GITHUB_REPOSITORY}/releases/${release_id}" \ | |
| -f prerelease=false \ | |
| -f draft=false >/dev/null |