mirror of
https://github.com/astromaddie/HaloCE-VR.git
synced 2026-10-23 08:00:12 +02:00
372 lines
14 KiB
YAML
372 lines
14 KiB
YAML
name: Build
|
||
|
||
# Debug and release builds of the native ports (Linux, Windows, Android) for
|
||
# every pushed commit, uploaded as artifacts. tools/ci_build.py does each
|
||
# build; see it for the options.
|
||
#
|
||
# Kept cheap:
|
||
# - one job per platform builds both configurations, so its set-up (tools,
|
||
# downloads, caches) is paid once;
|
||
# - compiles go through ccache, whose cache carries over from run to run, so
|
||
# a commit only recompiles what it changed; downloads (SDL, musl, Gradle)
|
||
# are cached too;
|
||
# - a newer push to the same branch cancels a run still going;
|
||
# - pushes that only change documentation do not build;
|
||
# - Windows, whose minutes cost twice Linux's, builds only Windows;
|
||
# - artifacts are kept for three days.
|
||
#
|
||
# A successful build of main is also published as a GitHub release, the
|
||
# newest marked latest, so that
|
||
# https://github.com/<repository>/releases/latest/download/<file> always
|
||
# gives the latest build; the last five are kept.
|
||
on:
|
||
push:
|
||
paths-ignore:
|
||
- "**.md"
|
||
workflow_dispatch:
|
||
|
||
concurrency:
|
||
group: build-${{ github.ref }}
|
||
cancel-in-progress: true
|
||
|
||
permissions:
|
||
contents: read
|
||
|
||
env:
|
||
CI_COMPILER_LAUNCHER: ccache
|
||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||
CCACHE_BASEDIR: ${{ github.workspace }}
|
||
CCACHE_MAXSIZE: 1G
|
||
|
||
jobs:
|
||
linux:
|
||
# Arch Linux has what the build needs as packages: clang 22 (for the
|
||
# profile-guided optimisation in pgo/) and 32-bit SDL 3.
|
||
runs-on: ubuntu-24.04
|
||
container: archlinux:latest
|
||
timeout-minutes: 40
|
||
steps:
|
||
- name: Install the toolchain
|
||
run: |
|
||
printf '\n[multilib]\nInclude = /etc/pacman.d/mirrorlist\n' >> /etc/pacman.conf
|
||
pacman -Syu --noconfirm --needed git zstd python python-pytest ninja ccache \
|
||
clang lld gcc lib32-glibc lib32-gcc-libs lib32-sdl3
|
||
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/cache@v4
|
||
with:
|
||
path: .ccache
|
||
key: ccache-linux-${{ github.sha }}
|
||
restore-keys: ccache-linux-
|
||
|
||
- name: Debug
|
||
run: python tools/ci_build.py linux debug
|
||
|
||
- name: Release
|
||
run: python tools/ci_build.py linux release
|
||
|
||
- name: Test
|
||
run: python -m pytest -q tools/test_linux_port.py
|
||
|
||
- name: Compile cache
|
||
run: ccache --show-stats
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
with:
|
||
name: halo-linux-debug
|
||
path: dist/halo-linux-debug
|
||
retention-days: 3
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
with:
|
||
name: halo-linux-release
|
||
path: dist/halo-linux-release
|
||
retention-days: 3
|
||
|
||
android:
|
||
# The runner image has the Android SDK and NDK, a JDK and CMake.
|
||
runs-on: ubuntu-24.04
|
||
timeout-minutes: 40
|
||
steps:
|
||
- name: Install the toolchain
|
||
run: |
|
||
mkdir -p "$RUNNER_TEMP/tools"
|
||
curl -fsSL -o ninja.zip https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip
|
||
unzip -q ninja.zip -d "$RUNNER_TEMP/tools" && rm ninja.zip
|
||
curl -fsSL https://github.com/ccache/ccache/releases/download/v4.10.2/ccache-4.10.2-linux-x86_64.tar.xz \
|
||
| tar -xJ -C "$RUNNER_TEMP/tools" --strip-components=1 ccache-4.10.2-linux-x86_64/ccache
|
||
echo "$RUNNER_TEMP/tools" >> "$GITHUB_PATH"
|
||
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/cache@v4
|
||
with:
|
||
path: .ccache
|
||
key: ccache-android-${{ github.sha }}
|
||
restore-keys: ccache-android-
|
||
|
||
# musl and SDL, which configure.py fetches
|
||
- uses: actions/cache@v4
|
||
with:
|
||
path: build/android/third_party
|
||
key: android-third-party-${{ hashFiles('tools/android_build.py') }}
|
||
|
||
- uses: actions/cache@v4
|
||
with:
|
||
path: |
|
||
~/.gradle/caches
|
||
~/.gradle/wrapper
|
||
key: gradle-${{ hashFiles('port/android/**/*.gradle*', 'port/android/gradle/wrapper/gradle-wrapper.properties') }}
|
||
|
||
# the app's signing key, from the ANDROID_KEYSTORE_BASE64 secret (a
|
||
# keystore, base64) and ANDROID_KEYSTORE_PASSWORD (its password, and
|
||
# the key's, alias halo): every build signed with it installs over the
|
||
# one before, as the app's self-updater (Updater.java) needs. Without
|
||
# them each build is signed with the runner's own debug key.
|
||
- name: Signing key
|
||
env:
|
||
KEYSTORE: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||
run: |
|
||
if [ -n "$KEYSTORE" ]; then
|
||
printf '%s' "$KEYSTORE" | base64 -d > "$RUNNER_TEMP/halo.keystore"
|
||
echo "HALO_ANDROID_KEYSTORE=$RUNNER_TEMP/halo.keystore" >> "$GITHUB_ENV"
|
||
else
|
||
echo "::warning::no ANDROID_KEYSTORE_BASE64 secret: the Android builds cannot update over each other"
|
||
fi
|
||
|
||
- name: Debug
|
||
env:
|
||
HALO_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||
run: python3 tools/ci_build.py android debug
|
||
|
||
- name: Release
|
||
env:
|
||
HALO_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||
run: python3 tools/ci_build.py android release
|
||
|
||
- name: Compile cache
|
||
run: ccache --show-stats
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
with:
|
||
name: halo-android-debug
|
||
path: dist/halo-android-debug
|
||
retention-days: 3
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
with:
|
||
name: halo-android-release
|
||
path: dist/halo-android-release
|
||
retention-days: 3
|
||
|
||
windows:
|
||
# The runner image has LLVM, Python and Visual Studio's x86 libraries and
|
||
# Windows SDK.
|
||
runs-on: windows-2025
|
||
timeout-minutes: 40
|
||
defaults:
|
||
run:
|
||
shell: bash
|
||
steps:
|
||
- name: Install the toolchain
|
||
shell: pwsh
|
||
run: |
|
||
Invoke-WebRequest -OutFile ninja.zip https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-win.zip
|
||
Expand-Archive ninja.zip -DestinationPath "$env:RUNNER_TEMP\tools"
|
||
Invoke-WebRequest -OutFile ccache.zip https://github.com/ccache/ccache/releases/download/v4.10.2/ccache-4.10.2-windows-x86_64.zip
|
||
Expand-Archive ccache.zip -DestinationPath $env:RUNNER_TEMP
|
||
Copy-Item "$env:RUNNER_TEMP\ccache-4.10.2-windows-x86_64\ccache.exe" "$env:RUNNER_TEMP\tools"
|
||
Remove-Item ninja.zip, ccache.zip
|
||
"$env:RUNNER_TEMP\tools" | Out-File -Append -Encoding utf8 $env:GITHUB_PATH
|
||
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/cache@v4
|
||
with:
|
||
path: .ccache
|
||
key: ccache-windows-${{ github.sha }}
|
||
restore-keys: ccache-windows-
|
||
|
||
# SDL's development package, which configure.py fetches
|
||
- uses: actions/cache@v4
|
||
with:
|
||
path: build/windows/third_party
|
||
key: windows-third-party-${{ hashFiles('tools/windows_build.py') }}
|
||
|
||
- name: Debug
|
||
run: python tools/ci_build.py windows debug
|
||
|
||
- name: Release
|
||
run: python tools/ci_build.py windows release
|
||
|
||
- name: Compile cache
|
||
run: ccache --show-stats
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
with:
|
||
name: halo-windows-debug
|
||
path: dist/halo-windows-debug
|
||
retention-days: 3
|
||
|
||
- uses: actions/upload-artifact@v4
|
||
with:
|
||
name: halo-windows-release
|
||
path: dist/halo-windows-release
|
||
retention-days: 3
|
||
|
||
release:
|
||
# The six builds as a release, tagged build-<run number> and marked latest
|
||
# (every file keeps its name from release to release, for the README's
|
||
# latest/download links); then all but the newest five build-* releases
|
||
# go, tags and all. Only for main, and only when every platform built.
|
||
needs: [linux, android, windows]
|
||
if: ${{ github.ref == 'refs/heads/main' }}
|
||
runs-on: ubuntu-24.04
|
||
timeout-minutes: 15
|
||
permissions:
|
||
contents: write
|
||
outputs:
|
||
tag: ${{ steps.publish.outputs.tag }}
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
GH_REPO: ${{ github.repository }}
|
||
steps:
|
||
- uses: actions/download-artifact@v4
|
||
with:
|
||
pattern: halo-*
|
||
path: artifacts
|
||
|
||
- name: Package
|
||
run: |
|
||
mkdir release
|
||
for build in artifacts/*/; do
|
||
name="$(basename "$build")"
|
||
# stored, not deflated: the updater in builds up to 57 waits for
|
||
# an end of stream the game's zlib 1.1 does not always report,
|
||
# and fails on a deflated entry that unpacked correctly
|
||
(cd "$build" && zip -0 -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
|
||
done
|
||
ls -l release
|
||
|
||
- name: Publish
|
||
id: publish
|
||
env:
|
||
MESSAGE: ${{ github.event.head_commit.message }}
|
||
COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}
|
||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||
run: |
|
||
tag="build-$GITHUB_RUN_NUMBER"
|
||
subject="$(printf '%s' "${MESSAGE:-manual run}" | head -n 1)"
|
||
printf '%s\n\nCommit: %s\nBuild: %s\n\nThe release builds are for playing; the debug builds stop at the first failed assertion and log it.\n' \
|
||
"$subject" "$COMMIT_URL" "$RUN_URL" > notes.md
|
||
gh release create "$tag" release/*.zip --target "$GITHUB_SHA" --latest \
|
||
--title "Build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
|
||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Keep the last five
|
||
run: |
|
||
gh release list --limit 100 --json tagName,createdAt \
|
||
--jq '[.[] | select(.tagName | startswith("build-"))] | sort_by(.createdAt) | reverse | .[5:] | .[].tagName' |
|
||
while read -r tag; do
|
||
gh release delete "$tag" --cleanup-tag --yes
|
||
done
|
||
|
||
notify:
|
||
# A Discord embed for the run, to the DISCORD_WEBHOOK secret (nothing
|
||
# without it): green when every platform built, red when one did not,
|
||
# with each platform's result, this run's release (main only), and links
|
||
# to the latest release's builds.
|
||
needs: [linux, android, windows, release]
|
||
if: ${{ !cancelled() }}
|
||
runs-on: ubuntu-24.04
|
||
timeout-minutes: 5
|
||
steps:
|
||
- name: Post to Discord
|
||
env:
|
||
WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||
LINUX: ${{ needs.linux.result }}
|
||
ANDROID: ${{ needs.android.result }}
|
||
WINDOWS: ${{ needs.windows.result }}
|
||
RELEASE_TAG: ${{ needs.release.outputs.tag }}
|
||
SERVER: ${{ github.server_url }}
|
||
REPOSITORY: ${{ github.repository }}
|
||
BRANCH: ${{ github.ref_name }}
|
||
SHA: ${{ github.sha }}
|
||
ACTOR: ${{ github.actor }}
|
||
MESSAGE: ${{ github.event.head_commit.message }}
|
||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||
COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}
|
||
run: |
|
||
if [ -z "$WEBHOOK" ]; then
|
||
echo "no DISCORD_WEBHOOK secret: nothing to post"
|
||
exit 0
|
||
fi
|
||
status() {
|
||
case "$1" in
|
||
success) echo "✅ Built" ;;
|
||
failure) echo "❌ Failed" ;;
|
||
cancelled) echo "⏹️ Cancelled" ;;
|
||
*) echo "➖ Skipped" ;;
|
||
esac
|
||
}
|
||
if [ "$LINUX" = success ] && [ "$ANDROID" = success ] && [ "$WINDOWS" = success ]; then
|
||
title="Build succeeded"; color=5763719
|
||
else
|
||
title="Build failed"; color=15548997
|
||
fi
|
||
latest="$SERVER/$REPOSITORY/releases/latest"
|
||
downloads="[Linux]($latest/download/halo-linux-release.zip) · [Windows]($latest/download/halo-windows-release.zip) · [Android]($latest/download/halo-android-release.zip) ([all builds]($latest))"
|
||
if [ -n "$RELEASE_TAG" ]; then
|
||
release="[$RELEASE_TAG]($SERVER/$REPOSITORY/releases/tag/$RELEASE_TAG)"
|
||
else
|
||
release="None (only a successful build of main makes one)"
|
||
fi
|
||
subject="$(printf '%s' "${MESSAGE:-manual run}" | head -n 1 | cut -c1-200)"
|
||
jq -n \
|
||
--arg title "$title" --argjson color "$color" --arg url "$RUN_URL" \
|
||
--arg description "[\`${SHA:0:7}\`]($COMMIT_URL) $subject — $ACTOR ($BRANCH)" \
|
||
--arg release "$release" --arg downloads "$downloads" \
|
||
--arg linux "$(status "$LINUX")" --arg windows "$(status "$WINDOWS")" --arg android "$(status "$ANDROID")" \
|
||
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||
'{embeds: [{
|
||
title: $title, url: $url, color: $color, description: $description, timestamp: $timestamp,
|
||
fields: [
|
||
{name: "Linux", value: $linux, inline: true},
|
||
{name: "Windows", value: $windows, inline: true},
|
||
{name: "Android", value: $android, inline: true},
|
||
{name: "Release", value: $release},
|
||
{name: "Latest release builds", value: $downloads}
|
||
]
|
||
}]}' > payload.json
|
||
# Discord rate limits webhooks (HTTP 429, with how many seconds to
|
||
# wait in retry_after): wait and try again, a few times. A post
|
||
# still limited, or a network or Discord error, is a warning rather
|
||
# than a failed run; any other answer (a deleted webhook) fails. (At
|
||
# most 5 tries of 15 seconds and 4 waits of 30: inside the job's 5
|
||
# minutes.)
|
||
for attempt in 1 2 3 4 5; do
|
||
code=$(curl -sS --max-time 15 -o response.json -w '%{http_code}' \
|
||
-H "Content-Type: application/json" -d @payload.json "$WEBHOOK") || code=000
|
||
case "$code" in
|
||
2??)
|
||
exit 0
|
||
;;
|
||
429)
|
||
wait=$(jq -r '.retry_after // 5' response.json 2>/dev/null || echo 5)
|
||
wait=$(awk -v w="$wait" 'BEGIN { w += 0; if (w < 1) w = 1; if (w > 30) w = 30; printf "%d", w + 0.999 }')
|
||
echo "Discord is rate limiting the webhook; trying again in ${wait}s (attempt $attempt of 5)"
|
||
;;
|
||
000|5??)
|
||
wait=5
|
||
echo "Discord answered HTTP $code; trying again in ${wait}s (attempt $attempt of 5)"
|
||
;;
|
||
*)
|
||
echo "::error::Discord answered HTTP $code: $(cat response.json 2>/dev/null)"
|
||
exit 1
|
||
;;
|
||
esac
|
||
[ "$attempt" -lt 5 ] && sleep "$wait"
|
||
done
|
||
echo "::warning::The Discord post did not go through after 5 attempts (HTTP $code)"
|