Skip to content

Release Build

Release Build #253

Workflow file for this run

name: Release Build
on:
push:
tags:
- '*.*.*'
- '!*-*'
workflow_dispatch:
inputs:
version:
description: "Version number (e.g. 1.0.0). Leave empty for auto-increment."
required: false
default: ""
type: string
platforms:
description: "Platforms to build"
required: true
default: "all"
type: choice
options:
- all
- android
- windows
- linux
- macos
- ios
create_release:
description: "Create a GitHub Release with the artifacts?"
required: false
default: true
type: boolean
keystore_base64:
description: "Base64-encoded keystore (from SAVE-THESE-SECRETS.txt). Overrides ANDROID_KEYSTORE_BASE64 secret."
required: false
default: ""
type: string
key_alias:
description: "Signing key alias (from SAVE-THESE-SECRETS.txt). Overrides ANDROID_SIGNING_KEY_ALIAS secret."
required: false
default: ""
type: string
key_password:
description: "Signing key password (from SAVE-THESE-SECRETS.txt). Overrides ANDROID_SIGNING_KEY_PASSWORD secret."
required: false
default: ""
type: string
permissions: {}
jobs:
# ═══════════════════════════════════════════════════════════════════════════════
# Determine version (shared across all platform jobs)
# ═══════════════════════════════════════════════════════════════════════════════
prepare:
name: Prepare
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
build_android: ${{ steps.platforms.outputs.android }}
build_windows: ${{ steps.platforms.outputs.windows }}
build_linux: ${{ steps.platforms.outputs.linux }}
build_macos: ${{ steps.platforms.outputs.macos }}
build_ios: ${{ steps.platforms.outputs.ios }}
steps:
- name: Determine version
id: version
run: |
INPUT_VERSION="${{ inputs.version }}"
REF="${{ github.ref_name }}"
if [[ -n "$INPUT_VERSION" && "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
VERSION="$INPUT_VERSION"
echo "Using manually specified version: $VERSION"
elif [[ "$REF" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
VERSION="$REF"
echo "Using version from tag: $VERSION"
else
VERSION="$(date -u +%Y).$(date -u +%-m%d).${{ github.run_number }}"
echo "Auto-generated version: $VERSION"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
- name: Determine platforms
id: platforms
run: |
PLATFORM="${{ inputs.platforms || 'all' }}"
if [ "$PLATFORM" = "all" ] || [ "$PLATFORM" = "android" ]; then echo "android=true" >> "$GITHUB_OUTPUT"; else echo "android=false" >> "$GITHUB_OUTPUT"; fi
if [ "$PLATFORM" = "all" ] || [ "$PLATFORM" = "windows" ]; then echo "windows=true" >> "$GITHUB_OUTPUT"; else echo "windows=false" >> "$GITHUB_OUTPUT"; fi
if [ "$PLATFORM" = "all" ] || [ "$PLATFORM" = "linux" ]; then echo "linux=true" >> "$GITHUB_OUTPUT"; else echo "linux=false" >> "$GITHUB_OUTPUT"; fi
if [ "$PLATFORM" = "all" ] || [ "$PLATFORM" = "macos" ]; then echo "macos=true" >> "$GITHUB_OUTPUT"; else echo "macos=false" >> "$GITHUB_OUTPUT"; fi
if [ "$PLATFORM" = "all" ] || [ "$PLATFORM" = "ios" ]; then echo "ios=true" >> "$GITHUB_OUTPUT"; else echo "ios=false" >> "$GITHUB_OUTPUT"; fi
# ═══════════════════════════════════════════════════════════════════════════════
# Android Build
# ═══════════════════════════════════════════════════════════════════════════════
build-android:
name: Build Android APK
needs: prepare
if: needs.prepare.outputs.build_android == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write
packages: read
outputs:
apk_artifact: ${{ steps.find_apk.outputs.artifact_name }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
- name: Install .NET 10.0.x
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"
- name: Install .NET Android workload
run: dotnet workload install android
- name: Authenticate to GitHub Packages
run: dotnet nuget update source winnerspiros-github --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text
- name: Ensure Android NDK and CMake are available
run: |
SDKMANAGER="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
yes | "$SDKMANAGER" --licenses > /dev/null 2>&1 || true
"$SDKMANAGER" --install "ndk;29.0.14206865" "cmake;3.22.1" > /dev/null 2>&1
- name: Build native library (libosu_native.so)
run: |
NDK_HOME="$ANDROID_HOME/ndk/29.0.14206865"
CMAKE_BIN="$ANDROID_HOME/cmake/3.22.1/bin/cmake"
for ABI in arm64-v8a; do
echo "::group::Building osu_native for $ABI"
CONFIGURE_ATTEMPTS=5
for attempt in $(seq 1 $CONFIGURE_ATTEMPTS); do
rm -rf "build-native/$ABI"
if "$CMAKE_BIN" -B "build-native/$ABI" -S osu.Android/Native \
-DCMAKE_TOOLCHAIN_FILE="$NDK_HOME/build/cmake/android.toolchain.cmake" \
-DANDROID_ABI="$ABI" \
-DANDROID_PLATFORM=android-33 \
-DCMAKE_BUILD_TYPE=Release; then
break
fi
if [ "$attempt" -eq "$CONFIGURE_ATTEMPTS" ]; then
echo "::error::CMake configure failed after $CONFIGURE_ATTEMPTS attempts"
exit 1
fi
echo "::warning::CMake configure attempt $attempt failed; retrying in $((attempt * 10))s"
sleep $((attempt * 10))
done
"$CMAKE_BIN" --build "build-native/$ABI" --config Release -j "$(nproc)"
mkdir -p "osu.Android/libs/$ABI"
cp "build-native/$ABI/libosu_native.so" "osu.Android/libs/$ABI/"
echo "::endgroup::"
done
echo "Native libraries built successfully:"
find osu.Android/libs -name "*.so" -exec ls -lh {} \;
- name: Decode or generate keystore
id: keystore
run: |
KS_PATH="${{ github.workspace }}/osu.Android/osu.keystore"
EFFECTIVE_KS="${INPUT_KEYSTORE_BASE64:-$KEYSTORE_BASE64}"
EFFECTIVE_ALIAS="${INPUT_KEY_ALIAS:-$KEY_ALIAS_SECRET}"
EFFECTIVE_PASS="${INPUT_KEY_PASSWORD:-$KEY_PASS_SECRET}"
EFFECTIVE_STORE_PASS="${INPUT_KEY_PASSWORD:-${STORE_PASS_SECRET:-$EFFECTIVE_PASS}}"
if [ -n "$EFFECTIVE_PASS" ]; then echo "::add-mask::$EFFECTIVE_PASS"; fi
if [ -n "$EFFECTIVE_STORE_PASS" ]; then echo "::add-mask::$EFFECTIVE_STORE_PASS"; fi
if [ -n "$EFFECTIVE_KS" ]; then
echo "$EFFECTIVE_KS" | base64 --decode > "$KS_PATH"
echo "has_keystore=true" >> "$GITHUB_OUTPUT"
echo "generated=false" >> "$GITHUB_OUTPUT"
echo "key_alias=${EFFECTIVE_ALIAS:-osu-release}" >> "$GITHUB_OUTPUT"
echo "key_pass=${EFFECTIVE_PASS}" >> "$GITHUB_OUTPUT"
echo "store_pass=${EFFECTIVE_STORE_PASS}" >> "$GITHUB_OUTPUT"
else
AUTO_PASS="osu-$(openssl rand -hex 12)"
echo "::add-mask::$AUTO_PASS"
keytool -genkeypair \
-keystore "$KS_PATH" \
-storepass "$AUTO_PASS" \
-keypass "$AUTO_PASS" \
-alias osu-release \
-keyalg RSA -keysize 2048 -validity 10000 \
-dname "CN=osu! Android,O=osu,C=US" 2>/dev/null
echo "has_keystore=true" >> "$GITHUB_OUTPUT"
echo "generated=true" >> "$GITHUB_OUTPUT"
echo "key_alias=osu-release" >> "$GITHUB_OUTPUT"
echo "key_pass=$AUTO_PASS" >> "$GITHUB_OUTPUT"
echo "store_pass=$AUTO_PASS" >> "$GITHUB_OUTPUT"
{
echo "=== osu! Android Signing Keystore ==="
echo ""
echo "ANDROID_KEYSTORE_BASE64 value (copy everything on the next line):"
base64 -w 0 "$KS_PATH"
echo ""
echo ""
echo "ANDROID_SIGNING_KEY_ALIAS value:"
echo "osu-release"
echo ""
echo "ANDROID_SIGNING_KEY_PASSWORD value:"
echo "$AUTO_PASS"
echo ""
echo "ANDROID_SIGNING_STORE_PASSWORD value:"
echo "$AUTO_PASS"
} > "${{ github.workspace }}/SAVE-THESE-SECRETS.txt"
echo "::warning::No signing keystore secret found — auto-generated one for this build."
fi
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEY_ALIAS_SECRET: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }}
KEY_PASS_SECRET: ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }}
STORE_PASS_SECRET: ${{ secrets.ANDROID_SIGNING_STORE_PASSWORD }}
INPUT_KEYSTORE_BASE64: ${{ inputs.keystore_base64 }}
INPUT_KEY_ALIAS: ${{ inputs.key_alias }}
INPUT_KEY_PASSWORD: ${{ inputs.key_password }}
- name: Install ffmpeg
run: |
sudo apt-get update
sudo apt-get install -y ffmpeg
- name: Clone and optimize all osu-resources from GitHub
run: |
git clone --depth=1 https://github.com/ppy/osu-resources /tmp/osu-resources
sed -i \
's|<PackageLicenseUrl>[^<]*</PackageLicenseUrl>|<PackageLicenseExpression>MIT</PackageLicenseExpression>|g' \
/tmp/osu-resources/osu.Game.Resources/osu.Game.Resources.csproj
python3 scripts/optimize_resource_overrides.py \
--root /tmp/osu-resources/osu.Game.Resources \
--config .github/resource-optimizer/osu-resources-config.json \
--report-file resource-budget/osu-resources-optimizer-report.json \
--summary-file resource-budget/osu-resources-optimizer-summary.md
RESOURCES_VERSION="$(date -u +%Y).$(date -u +%-m%d).${{ github.run_number }}"
if dotnet pack /tmp/osu-resources/osu.Game.Resources/osu.Game.Resources.csproj \
-c Release \
-o ./local-packages/ \
-p:Version="${RESOURCES_VERSION}"; then
sed -i \
"s|ppy.osu.Game.Resources\" Version=\"[^\"]*\"|ppy.osu.Game.Resources\" Version=\"${RESOURCES_VERSION}\"|" \
osu.Game/osu.Game.csproj
echo "✅ Using optimized osu.Game.Resources ${RESOURCES_VERSION} (local pack)"
else
echo "⚠️ dotnet pack failed; falling back to upstream ppy.osu.Game.Resources from NuGet"
fi
- name: Optimize local override media
run: |
python3 scripts/optimize_resource_overrides.py \
--root osu.Game/Resources \
--config .github/resource-optimizer/config.json \
--report-file resource-budget/optimizer-report.json \
--summary-file resource-budget/optimizer-summary.md
- name: Build Android APK
run: >
dotnet publish -c Release
osu.Android/osu.Android.csproj
-f net10.0-android
-p:Version="${{ needs.prepare.outputs.version }}"
-p:ApplicationDisplayVersion="${{ needs.prepare.outputs.version }}"
-p:ApplicationVersion="${{ github.run_number }}"
-p:AndroidKeyStore=true
-p:AndroidSigningKeyStore="${{ github.workspace }}/osu.Android/osu.keystore"
-p:AndroidSigningKeyAlias="${{ steps.keystore.outputs.key_alias }}"
-p:AndroidSigningKeyPass="${{ steps.keystore.outputs.key_pass }}"
-p:AndroidSigningStorePass="${{ steps.keystore.outputs.store_pass }}"
- name: Find and rename APK
id: find_apk
run: |
APK=$(find "osu.Android/bin/Release/net10.0-android/publish" -maxdepth 1 -name "*-Signed.apk" 2>/dev/null | head -1)
if [ -z "$APK" ]; then
APK=$(find "osu.Android/bin/Release" -name "*-Signed.apk" | head -1)
fi
if [ -z "$APK" ]; then
echo "::error::Failed to locate signed APK."
find osu.Android/bin -name "*.apk" -o -name "*.aab" 2>/dev/null || true
exit 1
fi
VERSION="${{ needs.prepare.outputs.version }}"
FINAL_APK="$(dirname "$APK")/osu-lazer-$VERSION.apk"
cp "$APK" "$FINAL_APK"
APK_SIZE=$(stat -c %s "$FINAL_APK" 2>/dev/null || stat -f %z "$FINAL_APK" 2>/dev/null || wc -c < "$FINAL_APK")
APK_SIZE_MB=$((APK_SIZE / 1048576))
echo "Found APK: $FINAL_APK ($APK_SIZE_MB MB)"
echo "apk_path=$FINAL_APK" >> "$GITHUB_OUTPUT"
echo "artifact_name=osu-android-$VERSION" >> "$GITHUB_OUTPUT"
- name: Verify APK signature
run: |
APK="${{ steps.find_apk.outputs.apk_path }}"
APKSIGNER="$ANDROID_HOME/build-tools/$(ls "$ANDROID_HOME/build-tools" | sort -V | tail -1)/apksigner"
"$APKSIGNER" verify --print-certs "$APK"
- name: Verify native libraries in APK
run: |
APK="${{ steps.find_apk.outputs.apk_path }}"
NATIVE_LIBS=$(unzip -l "$APK" | grep "lib/arm64-v8a/.*\.so" || true)
MISSING=0
for LIB in libbass.so libbass_fx.so libbassmix.so; do
if echo "$NATIVE_LIBS" | grep -q "$LIB"; then
echo "✅ $LIB found"
else
echo "::error::$LIB is MISSING from the APK."
MISSING=1
fi
done
[ "$MISSING" -eq 0 ] || exit 1
# Verify not Linux glibc ELFs
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
BAD=0
for LIB in libbass.so libbass_fx.so libbassmix.so; do
unzip -p "$APK" "lib/arm64-v8a/$LIB" > "$TMPDIR/$LIB"
if strings "$TMPDIR/$LIB" | grep -q "^GLIBC_"; then
echo "::error::$LIB references GLIBC_ — this is a Linux ELF, not Android."
BAD=1
fi
done
[ "$BAD" -eq 0 ] || exit 1
echo "All native libraries valid ✓"
- name: Upload APK artifact
uses: actions/upload-artifact@v7
with:
name: osu-android-${{ needs.prepare.outputs.version }}
path: ${{ steps.find_apk.outputs.apk_path }}
if-no-files-found: error
- name: Upload generated keystore
if: steps.keystore.outputs.generated == 'true'
uses: actions/upload-artifact@v7
with:
name: osu-signing-keystore
path: |
${{ github.workspace }}/osu.Android/osu.keystore
${{ github.workspace }}/SAVE-THESE-SECRETS.txt
retention-days: 7
# ═══════════════════════════════════════════════════════════════════════════════
# Windows Build
# ═══════════════════════════════════════════════════════════════════════════════
build-windows:
name: Build Windows
needs: prepare
if: needs.prepare.outputs.build_windows == 'true'
runs-on: windows-latest
timeout-minutes: 45
permissions:
contents: write
packages: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install .NET 10.0.x
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"
- name: Authenticate to GitHub Packages
run: dotnet nuget update source winnerspiros-github --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text
- name: Publish Windows x64
run: >
dotnet publish -c Release
osu.Desktop/osu.Desktop.csproj
-r win-x64
--self-contained
-p:Version="${{ needs.prepare.outputs.version }}"
-p:PublishSingleFile=true
-p:PublishTrimmed=true
-p:TrimMode=partial
-p:EnableCompressionInSingleFile=true
-p:IncludeNativeLibrariesForSelfExtract=true
-p:DebugType=none
-p:DebugSymbols=false
-p:ServerGarbageCollection=true
-p:ConcurrentGarbageCollection=true
-o artifacts/win-x64
- name: Package Windows build
run: |
$version = "${{ needs.prepare.outputs.version }}"
Compress-Archive -Path artifacts/win-x64/* -DestinationPath "artifacts/osu-lazer-win-x64-$version.zip"
- name: Upload Windows artifact
uses: actions/upload-artifact@v7
with:
name: osu-windows-${{ needs.prepare.outputs.version }}
path: artifacts/osu-lazer-win-x64-*.zip
if-no-files-found: error
# ═══════════════════════════════════════════════════════════════════════════════
# Linux Build
# ═══════════════════════════════════════════════════════════════════════════════
build-linux:
name: Build Linux
needs: prepare
if: needs.prepare.outputs.build_linux == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: write
packages: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install .NET 10.0.x
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"
- name: Authenticate to GitHub Packages
run: dotnet nuget update source winnerspiros-github --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text
- name: Publish Linux x64
run: >
dotnet publish -c Release
osu.Desktop/osu.Desktop.csproj
-r linux-x64
--self-contained
-p:Version="${{ needs.prepare.outputs.version }}"
-p:PublishSingleFile=true
-p:PublishTrimmed=true
-p:TrimMode=partial
-p:EnableCompressionInSingleFile=true
-p:IncludeNativeLibrariesForSelfExtract=true
-p:DebugType=none
-p:DebugSymbols=false
-p:ServerGarbageCollection=true
-p:ConcurrentGarbageCollection=true
-o artifacts/linux-x64
- name: Package Linux build
run: |
VERSION="${{ needs.prepare.outputs.version }}"
chmod +x artifacts/linux-x64/osu!
tar -czf "artifacts/osu-lazer-linux-x64-$VERSION.tar.gz" -C artifacts/linux-x64 .
- name: Upload Linux artifact
uses: actions/upload-artifact@v7
with:
name: osu-linux-${{ needs.prepare.outputs.version }}
path: artifacts/osu-lazer-linux-x64-*.tar.gz
if-no-files-found: error
# ═══════════════════════════════════════════════════════════════════════════════
# macOS Build
# ═══════════════════════════════════════════════════════════════════════════════
build-macos:
name: Build macOS
needs: prepare
if: needs.prepare.outputs.build_macos == 'true'
runs-on: macos-latest
timeout-minutes: 45
permissions:
contents: write
packages: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install .NET 10.0.x
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"
- name: Authenticate to GitHub Packages
run: dotnet nuget update source winnerspiros-github --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text
- name: Publish macOS arm64
run: >
dotnet publish -c Release
osu.Desktop/osu.Desktop.csproj
-r osx-arm64
--self-contained
-p:Version="${{ needs.prepare.outputs.version }}"
-p:PublishSingleFile=true
-p:PublishTrimmed=true
-p:TrimMode=partial
-p:EnableCompressionInSingleFile=true
-p:IncludeNativeLibrariesForSelfExtract=true
-p:DebugType=none
-p:DebugSymbols=false
-p:ServerGarbageCollection=true
-p:ConcurrentGarbageCollection=true
-o artifacts/osx-arm64
- name: Publish macOS x64
run: >
dotnet publish -c Release
osu.Desktop/osu.Desktop.csproj
-r osx-x64
--self-contained
-p:Version="${{ needs.prepare.outputs.version }}"
-p:PublishSingleFile=true
-p:PublishTrimmed=true
-p:TrimMode=partial
-p:EnableCompressionInSingleFile=true
-p:IncludeNativeLibrariesForSelfExtract=true
-p:DebugType=none
-p:DebugSymbols=false
-p:ServerGarbageCollection=true
-p:ConcurrentGarbageCollection=true
-o artifacts/osx-x64
- name: Package macOS builds
run: |
VERSION="${{ needs.prepare.outputs.version }}"
chmod +x "artifacts/osx-arm64/osu!"
chmod +x "artifacts/osx-x64/osu!"
tar -czf "artifacts/osu-lazer-macos-arm64-$VERSION.tar.gz" -C artifacts/osx-arm64 .
tar -czf "artifacts/osu-lazer-macos-x64-$VERSION.tar.gz" -C artifacts/osx-x64 .
- name: Upload macOS artifact
uses: actions/upload-artifact@v7
with:
name: osu-macos-${{ needs.prepare.outputs.version }}
path: artifacts/osu-lazer-macos-*.tar.gz
if-no-files-found: error
# ═══════════════════════════════════════════════════════════════════════════════
# iOS Build
# ═══════════════════════════════════════════════════════════════════════════════
build-ios:
name: Build iOS
needs: prepare
if: needs.prepare.outputs.build_ios == 'true'
runs-on: macos-latest
timeout-minutes: 60
permissions:
contents: write
packages: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install .NET 10.0.x
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"
- name: Install .NET iOS workload
run: dotnet workload install ios
- name: Authenticate to GitHub Packages
run: dotnet nuget update source winnerspiros-github --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text
- name: Build iOS (unsigned .app)
run: >
dotnet publish -c Release
osu.iOS/osu.iOS.csproj
-f net10.0-ios
-p:Version="${{ needs.prepare.outputs.version }}"
-p:ApplicationDisplayVersion="${{ needs.prepare.outputs.version }}"
-p:ApplicationVersion="${{ github.run_number }}"
-p:ArchiveOnBuild=false
-p:BuildIpa=false
-p:CodesignKey=""
-p:CodesignProvision=""
- name: Package iOS build
run: |
VERSION="${{ needs.prepare.outputs.version }}"
mkdir -p artifacts
APP_PATH=$(find osu.iOS/bin/Release -name "*.app" -type d | head -1)
if [ -z "$APP_PATH" ]; then
echo "::error::Could not find .app bundle"
exit 1
fi
tar -czf "artifacts/osu-lazer-ios-$VERSION.tar.gz" -C "$(dirname "$APP_PATH")" "$(basename "$APP_PATH")"
- name: Upload iOS artifact
uses: actions/upload-artifact@v7
with:
name: osu-ios-${{ needs.prepare.outputs.version }}
path: artifacts/osu-lazer-ios-*.tar.gz
if-no-files-found: error
# ═══════════════════════════════════════════════════════════════════════════════
# Create GitHub Release
# ═══════════════════════════════════════════════════════════════════════════════
release:
name: Create Release
needs: [prepare, build-android, build-windows, build-linux, build-macos, build-ios]
if: |
always() &&
(inputs.create_release == true || github.event_name == 'push') &&
!contains(needs.*.result, 'failure') &&
!contains(needs.*.result, 'cancelled')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: release-artifacts
pattern: osu-*-${{ needs.prepare.outputs.version }}
merge-multiple: true
continue-on-error: true
- name: Verify artifacts exist
run: |
if [ ! -d release-artifacts ] || [ -z "$(ls -A release-artifacts 2>/dev/null)" ]; then
echo "::error::No release artifacts found"
exit 1
fi
echo "Release artifacts:"
find release-artifacts -type f | sort
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ needs.prepare.outputs.tag }}
name: "osu! ${{ needs.prepare.outputs.version }}"
files: release-artifacts/*
generate_release_notes: true
body: |
## osu! v${{ needs.prepare.outputs.version }}
### Downloads
| Platform | File |
|---|---|
| Android (arm64) | `osu-lazer-*.apk` |
| Windows (x64) | `osu-lazer-win-x64-*.zip` |
| Linux (x64) | `osu-lazer-linux-x64-*.tar.gz` |
| macOS (Apple Silicon) | `osu-lazer-macos-arm64-*.tar.gz` |
| macOS (Intel) | `osu-lazer-macos-x64-*.tar.gz` |
| iOS (unsigned) | `osu-lazer-ios-*.tar.gz` |
> **Android:** Requires Android 13+ (arm64). Enable "Install from unknown sources" if prompted.
> **iOS:** Unsigned build — requires sideloading via AltStore, Sideloadly, or Xcode.
> **Desktop:** Self-contained, no .NET runtime needed.
make_latest: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}