Skip to content

Merge pull request #434 from s3anyboy/controller_layouts #289

Merge pull request #434 from s3anyboy/controller_layouts

Merge pull request #434 from s3anyboy/controller_layouts #289

Workflow file for this run

name: Build
# Debug and release builds of the native ports (Linux, Windows, Android) and
# the web port 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,
# the Linux build's system root) 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. Its web build is then the
# site on GitHub Pages (https://halocombatevolved.com/, port/web/README.md).
on:
push:
paths-ignore:
- "**.md"
workflow_dispatch:
inputs:
profile:
description: "Also build the profiling flavour (configure.py --profile) on Linux"
type: boolean
default: false
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 CMake, pkg-config and
# wayland-scanner for the SDL 3 that the portable build brings. The build
# links against Debian 11's 32-bit glibc (tools/linux_sysroot.py), not
# Arch's, so that it starts on SteamOS and older distributions; Arch's
# 32-bit glibc is for the tests.
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 cmake pkgconf wayland
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: .ccache
key: ccache-linux-${{ github.sha }}
restore-keys: ccache-linux-
# the system root and SDL's source, which the build downloads
- uses: actions/cache@v4
with:
path: build/linux/third_party
key: linux-third-party-${{ hashFiles('tools/linux_sysroot.py') }}
- name: Debug
run: python tools/ci_build.py linux debug
# (the profiling build, configure.py --profile, which no other step
# compiles: only when a run started by hand asks for it, as it costs a
# build of its own)
- name: Profile
if: github.event_name == 'workflow_dispatch' && inputs.profile
run: python tools/ci_build.py linux profile
- name: Release
run: python tools/ci_build.py linux release
- name: Test
run: |
python -m pytest -q tools/test_linux_port.py tools/test_android_asm_convert.py tools/test_android_mirror_uploads.py tools/test_android_ce_texture_cache.py tools/test_host_watch_hash.py tools/test_touch_menu.py tools/harness tools/test_profile.py tools/stagekit/tests tools/test_net_compare.py tools/test_server_sim.py tools/test_egg.py tools/test_deploy_gametypes.py
# the dedicated server: --version, and --server without maps refused (exit code 2); its cases with
# clients need the retail maps, and are skipped
python -m pytest -q tools/test_dedicated_server.py
python tools/test_light_storage.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"
# (each download checked against the hash it had when it was pinned)
curl -fsSL -o ninja.zip https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip
curl -fsSL -o ccache.tar.xz https://github.com/ccache/ccache/releases/download/v4.10.2/ccache-4.10.2-linux-x86_64.tar.xz
sha256sum -c - <<'EOF'
6f98805688d19672bd699fbbfa2c2cf0fc054ac3df1f0e6a47664d963d530255 ninja.zip
80cab87bd510eca796467aee8e663c398239e0df1c4800a0b5dff11dca0b4f18 ccache.tar.xz
EOF
unzip -q ninja.zip -d "$RUNNER_TEMP/tools" && rm ninja.zip
tar -xJf ccache.tar.xz -C "$RUNNER_TEMP/tools" --strip-components=1 ccache-4.10.2-linux-x86_64/ccache && rm ccache.tar.xz
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', 'port/android/patches/**') }}
- 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
# the touch controls' layout and gyroscope maths (the runner's JDK)
- name: Test the touch layout
run: python3 tools/test_touch_layout.py
- 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
Invoke-WebRequest -OutFile ccache.zip https://github.com/ccache/ccache/releases/download/v4.10.2/ccache-4.10.2-windows-x86_64.zip
# (each download checked against the hash it had when it was pinned)
if ((Get-FileHash ninja.zip -Algorithm SHA256).Hash -ne "F550FEC705B6D6FF58F2DB3C374C2277A37691678D6ABA463ADCBB129108467A") { throw "ninja.zip: unexpected SHA-256" }
if ((Get-FileHash ccache.zip -Algorithm SHA256).Hash -ne "6252F081876A9A9F700FAE13A5AEC5D0D486B28261D7F1F72AC11C7AD9DF4DA9") { throw "ccache.zip: unexpected SHA-256" }
Expand-Archive ninja.zip -DestinationPath "$env:RUNNER_TEMP\tools"
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: Test light storage
run: python tools/test_light_storage.py
- 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
# the PDBs, for crash reports' backtraces (tools/ci_build.py)
- uses: actions/upload-artifact@v4
with:
name: halo-windows-debug-symbols
path: dist/halo-windows-debug-symbols
retention-days: 3
- uses: actions/upload-artifact@v4
with:
name: halo-windows-release-symbols
path: dist/halo-windows-release-symbols
retention-days: 3
web:
# Emscripten (emsdk, the version below, cached) builds the web port's
# site (port/web/README.md); the tests check its graph, the page's
# importer and service worker, its sockets, that it starts in a
# headless Chromium (with no game data, as far as the menus' map), and
# that two browsers join a room through a local MQTT broker.
runs-on: ubuntu-24.04
timeout-minutes: 40
env:
EMSDK_VERSION: 6.0.10
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/emsdk
key: emsdk-${{ env.EMSDK_VERSION }}-${{ hashFiles('port/web/halo_sdl3.py') }}
- name: Install the toolchain
run: |
if [ ! -x ~/emsdk/upstream/emscripten/emcc ]; then
rm -rf ~/emsdk
git clone --depth 1 https://github.com/emscripten-core/emsdk.git ~/emsdk
~/emsdk/emsdk install "$EMSDK_VERSION"
fi
~/emsdk/emsdk activate "$EMSDK_VERSION"
sudo apt-get update -q
sudo apt-get install -y -q ninja-build ccache python3-pytest mosquitto
- uses: actions/cache@v4
with:
path: .ccache
key: ccache-web-${{ github.sha }}
restore-keys: ccache-web-
- name: Debug
run: |
source ~/emsdk/emsdk_env.sh
python3 tools/ci_build.py web debug
- name: Release
run: |
source ~/emsdk/emsdk_env.sh
python3 tools/ci_build.py web release
- name: Test
run: |
source ~/emsdk/emsdk_env.sh
node --test "port/web/tests/*.test.js"
# (the dedicated server's admin page, port/assets/server_ui, against tools/server_sim.py)
node --test tools/server_ui_tests/
python3 -m pytest -q tools/test_web_build.py
- name: Test in Chromium and Firefox
run: |
npm install --no-save --prefix "$RUNNER_TEMP/playwright" playwright@1.58
"$RUNNER_TEMP/playwright/node_modules/.bin/playwright" install --with-deps chromium firefox
NODE_PATH="$RUNNER_TEMP/playwright/node_modules" node port/web/tests/smoke.mjs dist/halo-web-release
NODE_PATH="$RUNNER_TEMP/playwright/node_modules" node port/web/tests/rooms.mjs dist/halo-web-release
# (the native builds' WebRTC, port/linux/src/p2p_webrtc.c, with both)
NODE_PATH="$RUNNER_TEMP/playwright/node_modules" node port/web/tests/webrtc_native.mjs chromium firefox
- name: Compile cache
run: ccache --show-stats
- uses: actions/upload-artifact@v4
with:
name: halo-web-debug
path: dist/halo-web-debug
include-hidden-files: true
retention-days: 3
- uses: actions/upload-artifact@v4
with:
name: halo-web-release
path: dist/halo-web-release
include-hidden-files: true
retention-days: 3
release:
# The eight builds (and the Windows builds' PDBs) 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), with their SHA256SUMS and, when the
# RELEASE_SIGNING_KEY secret is set, its Ed25519 signature by the release
# key (SHA256SUMS.sig; the public key is deploy/release-key.pub); 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, web]
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:
# (tools/ci_build.py and tools/release_sign.sh, and the public key)
- uses: actions/checkout@v4
with:
sparse-checkout: |
tools/ci_build.py
tools/release_sign.sh
deploy/release-key.pub
sparse-checkout-cone-mode: false
- uses: actions/download-artifact@v4
with:
pattern: halo-*
path: artifacts
- name: Package
run: |
mkdir release
for build in artifacts/*/; do
name="$(basename "$build")"
(cd "$build" && zip -qr "$GITHUB_WORKSPACE/release/$name.zip" .)
done
python3 tools/ci_build.py sums release
ls -l release
# SHA256SUMS signed with the release key's seed (the RELEASE_SIGNING_KEY
# secret, 64 hex digits), and the signature checked against
# deploy/release-key.pub before anything is published: a secret that is
# not the key of the public key in the repository stops the release
- name: Sign the sums
env:
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
run: |
if [ -z "$RELEASE_SIGNING_KEY" ]; then
echo "::warning::no RELEASE_SIGNING_KEY secret: the release's SHA256SUMS is not signed"
exit 0
fi
sh tools/release_sign.sh sign release/SHA256SUMS
sh tools/release_sign.sh verify release/SHA256SUMS
- 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/* --target "$GITHUB_SHA" --latest \
--title "Build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
echo "tag=$tag" >> "$GITHUB_OUTPUT"
# The Windows builds' executables and PDBs to Sentry, which turns the
# crash reports of these builds (port/windows/src/win32_crash.c) into
# backtraces with function names and lines. Needs the SENTRY_AUTH_TOKEN
# secret: an organization auth token of the Sentry organization, which
# names the organization and its region itself. The SENTRY_PROJECT
# variable can name the project (its slug), else the id in the DSN of
# win32_crash.c; SENTRY_ORG and SENTRY_URL (https://de.sentry.io) are
# for a personal token instead. A failed upload does not stop the
# release.
- name: Upload Windows symbols to Sentry
continue-on-error: true
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT || '4512207917744208' }}
ORG: ${{ vars.SENTRY_ORG }}
URL: ${{ vars.SENTRY_URL }}
SENTRY_CLI_VERSION: 2.46.0
run: |
if [ -z "$SENTRY_AUTH_TOKEN" ]; then
echo "::warning::no SENTRY_AUTH_TOKEN secret: crash reports of this build will have no function names"
exit 0
fi
if [ -n "$ORG" ]; then export SENTRY_ORG="$ORG"; fi
if [ -n "$URL" ]; then export SENTRY_URL="$URL"; fi
curl -fsSL -o sentry-cli "https://github.com/getsentry/sentry-cli/releases/download/$SENTRY_CLI_VERSION/sentry-cli-Linux-x86_64"
chmod +x sentry-cli
./sentry-cli debug-files upload --wait \
artifacts/halo-windows-release artifacts/halo-windows-release-symbols \
artifacts/halo-windows-debug artifacts/halo-windows-debug-symbols
- 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
pages:
# The release's web build to GitHub Pages, which serves it at
# https://halocombatevolved.com/ (port/web/README.md, "Hosting"). Only
# main, only once every platform built and the release is out (the site
# and the release are one build), and a deployment is never cancelled
# halfway. The repository's Pages source must be GitHub Actions.
needs: [release]
if: ${{ github.ref == 'refs/heads/main' }}
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- uses: actions/download-artifact@v4
with:
name: halo-web-release
path: site
# The game's wasm, most of a build, from jsDelivr rather than GitHub
# Pages, which serves 100 GB a month: the wasm alone in a commit of the
# web-wasm branch (pushed over the last, so the repository keeps one),
# which jsDelivr serves by its hash (with CORS; GitHub's release
# downloads have none, so a page cannot fetch them), and version.json
# names that copy and its SHA-256. The site's service worker takes the
# wasm from there, checked, and from the site only when it cannot
# (port/web/site/sw.js). A failure here leaves the site serving it.
- name: The wasm on jsDelivr
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
sha256="$(sha256sum site/halo.wasm | cut -d' ' -f1)"
mkdir wasm
cp site/halo.wasm wasm/
cd wasm
git init -q -b web-wasm
git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
add halo.wasm
git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
commit -q -m "The web build's wasm (build $GITHUB_RUN_NUMBER, $GITHUB_SHA)"
git push -q --force "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:refs/heads/web-wasm
url="https://cdn.jsdelivr.net/gh/${GITHUB_REPOSITORY}@$(git rev-parse HEAD)/halo.wasm"
# (jsDelivr fetches it now, and it must be the same file: it serves
# files of at most 20 MB from GitHub)
curl -fsSL --retry 6 --retry-delay 10 --retry-all-errors -o served.wasm "$url"
echo "$sha256 served.wasm" | sha256sum -c -
cd ..
python3 - "$url" "$sha256" <<'EOF'
import json, sys
with open("site/version.json") as file:
stamp = json.load(file)
stamp["wasm"] = {"url": sys.argv[1], "sha256": sys.argv[2]}
with open("site/version.json", "w") as file:
json.dump(stamp, file, indent=1)
EOF
echo "the wasm from $url"
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: site
- id: deploy
uses: actions/deploy-pages@v4
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 and the site.
needs: [linux, android, windows, web, release, pages]
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 }}
WEB: ${{ needs.web.result }}
PAGES: ${{ needs.pages.result }}
SITE_URL: https://halocombatevolved.com/
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 ] && [ "$WEB" = 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) · [Web]($SITE_URL) ([all builds]($latest))"
if [ -n "$RELEASE_TAG" ]; then
release="[$RELEASE_TAG]($SERVER/$REPOSITORY/releases/tag/$RELEASE_TAG)"
if [ "$PAGES" = success ]; then
release="$release, on [the site]($SITE_URL)"
fi
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 web "$(status "$WEB")" \
--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: "Web", value: $web, 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)"