Skip to content

Manual Releases

Manual Releases #7

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