releases: local build+upload script, optional Forgejo Actions workflow, docs

Clarifies that releases ship ONLY the recompiled binaries (skate3 +
librexruntime + ghost_relay + README) - never the game files; the game dump
(default.xex + EAWebkit.xex) is required at BUILD TIME for codegen only.

- tools/release/release.sh: builds + packages a per-platform release zip
  (configure -> codegen -> build -> zip), prints a sha256sum, and with
  --upload creates/updates a Forgejo Release and attaches the zip via the
  API (GITEA_TOKEN). Supports linux/windows/macos presets on their native
  hosts.
- .forgejo/workflows/release.yml: optional automation - builds on a
  self-hosted Docker runner (ubuntu-24.04 label), self-heals the broken
  nested submodule pins via tools/ci/init-submodules.sh, and publishes a
  Linux release on v* tags. Needs the game dump mounted on the runner
  (codegen only, never uploaded), SUBMODULE_KEY + RELEASE_TOKEN secrets.
- tools/ci/init-submodules.sh: recursive submodule init that self-heals
  upstream pins a fresh clone can no longer fetch (the rexglue fork pins
  thirdparty/imgui to a commit ocornut/imgui dropped; the script checks out
  the upstream default tip for any nested submodule left empty).
- docs/releases.md: what's in a release, the local path, and the optional
  CI path.
- .gitignore: keep tools/ci + tools/release, ignore local release zips.
This commit is contained in:
MiTHRAL 2026-08-26 23:32:13 -04:00
parent 32c5e438ed
commit 7173d7e88d
5 changed files with 420 additions and 1 deletions

View file

@ -0,0 +1,126 @@
name: release
# Builds a Linux release of the game and attaches it to a Forgejo/Gitea
# Release, so players can download a ready-to-run build and only need to
# bring their own Skate 3 ISO (the built-in installer extracts it).
#
# Runs on the self-hosted runner (see tools/ci/setup_runner.sh and
# docs/ci-releases.md). The runner must have the extracted game dump at
# /opt/skate3-game (default.xex + data/webkit/EAWebkit.xex, and optionally
# the TU_12K2276_* title-update package). The game files are used only for
# codegen; they are NOT bundled into the release artifact.
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build-linux:
# Match this label to your self-hosted runner's label (Docker runners
# commonly use ubuntu-latest / ubuntu-22.04 / ubuntu-24.04).
runs-on: ubuntu-24.04
env:
GAME_ROOT: /opt/skate3-game
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
with:
# Submodules are initialized manually so we can self-heal the
# broken upstream pins (see tools/ci/init-submodules.sh).
submodules: false
- name: Init submodules
shell: bash
env:
SUBMODULE_KEY: ${{ secrets.SUBMODULE_KEY }}
run: |
set -euo pipefail
# SSH key for the self-hosted rexglue fork submodule
# (git@git.mithraic.cloud:ad3laid3/rexglue-skate3.git).
if [ -n "${SUBMODULE_KEY:-}" ]; then
mkdir -p ~/.ssh && chmod 700 ~/.ssh
printf '%s\n' "$SUBMODULE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan git.mithraic.cloud >> ~/.ssh/known_hosts 2>/dev/null || true
fi
git submodule update --init --jobs 4
bash tools/ci/init-submodules.sh
- name: Install build dependencies
shell: bash
run: |
# Docker runner containers usually run as root (no sudo).
if command -v sudo >/dev/null 2>&1 && [ "$(id -u)" != 0 ]; then
APT="sudo apt-get"
else
APT="apt-get"
fi
$APT update
$APT install -y --no-install-recommends \
git cmake ninja-build pkg-config p7zip-full zip \
clang-20 lld-20 \
libgtk-3-dev libx11-xcb-dev \
libvulkan-dev vulkan-tools mesa-vulkan-drivers \
libasound2-dev libpulse-dev libpipewire-0.3-dev libudev-dev
- name: Configure
shell: bash
run: |
set -euo pipefail
TU=""
if compgen -G "$GAME_ROOT"/TU_12K2276_* >/dev/null; then
TU="-DSKATE3_TITLE_UPDATE_PACKAGE=$(compgen -G "$GAME_ROOT"/TU_12K2276_* | head -1)"
fi
echo "TU_ARGS=$TU" >> "$GITHUB_ENV"
cmake --preset linux-relwithdebinfo -DSKATE3_GAME_DATA_ROOT="$GAME_ROOT" $TU
- name: Generate recompiled code
shell: bash
run: cmake --build --preset linux-relwithdebinfo --target generate-all --parallel
- name: Reconfigure (generated sources)
shell: bash
run: |
set -euo pipefail
TU="${TU_ARGS:-}"
cmake --preset linux-relwithdebinfo -DSKATE3_GAME_DATA_ROOT="$GAME_ROOT" $TU
- name: Build
shell: bash
run: cmake --build --preset linux-relwithdebinfo --parallel
- name: Package release zip
shell: bash
run: |
set -euo pipefail
OUT="out/build/linux-relwithdebinfo"
VER="${GITHUB_REF_NAME#v}"
[ -n "$VER" ] || VER="dev"
NAME="Skate3Recomp-Linux-$VER"
STAGE="$NAME"
rm -rf "$STAGE"; mkdir -p "$STAGE"
cp "$OUT/skate3" "$STAGE/"
cp "$OUT/librexruntime.so" "$STAGE/" 2>/dev/null || true
cp "$OUT/ghost_relay" "$STAGE/" 2>/dev/null || true
# Title-update patches staged next to the binary when the build
# used the TU package (the CMake stage target places them here).
if [ -f "$OUT/game/default.xexp" ]; then
mkdir -p "$STAGE/game/data/webkit"
cp "$OUT/game/default.xexp" "$STAGE/game/"
cp "$OUT/game/data/webkit/EAWebkit.xexp" "$STAGE/game/data/webkit/" 2>/dev/null || true
fi
cp README.md "$STAGE/" 2>/dev/null || true
zip -r "$NAME.zip" "$STAGE"
echo "ARTIFACT=$NAME.zip" >> "$GITHUB_ENV"
- name: Publish release
uses: https://code.forgejo.org/forgejo/release-action@v2
with:
token: ${{ secrets.RELEASE_TOKEN }}
files: ${{ env.ARTIFACT }}
tag: ${{ github.ref_name }}
title: "Skate 3 Recomp ${{ github.ref_name }}"
draft: false
prerelease: false

7
.gitignore vendored
View file

@ -4,10 +4,15 @@
/generated/ /generated/
/generated.backup-*/ /generated.backup-*/
/tools/* /tools/*
# ...except the ghost multiplayer relay server (a first-party tool). # ...except the ghost multiplayer relay server + release/CI tooling (first-party).
!/tools/ghost_relay_main.cpp !/tools/ghost_relay_main.cpp
!/tools/ghost_relay.service !/tools/ghost_relay.service
!/tools/ghost_relay_deploy.sh !/tools/ghost_relay_deploy.sh
!/tools/ci/
!/tools/release/
# Local release zips/stages are build products.
Skate3Recomp-*.zip
/Skate3Recomp-*/
# Local game dump and retail media. Users must provide their own copy. # Local game dump and retail media. Users must provide their own copy.
/game/ /game/

104
docs/releases.md Normal file
View file

@ -0,0 +1,104 @@
# Releases
Releases are prebuilt binaries that players download so they only need their
own Skate 3 ISO — **no building from scratch**. This project does not and
cannot ship game content; the release zip is just the recompiled binaries.
## What's in a release (and what isn't)
A release zip (`Skate3Recomp-Linux-<ver>.zip`) contains only:
- `skate3` — the recompiled game binary (also the in-app **ISO installer**)
- `librexruntime.so` (Linux) / `librexruntime.dll` (Windows) /
`librexruntime.dylib` (macOS)
- `ghost_relay` — the multiplayer relay server binary
- `README.md`
- `game/default.xexp` + `game/data/webkit/EAWebkit.xexp` — **only** when the
build used the Title Update package (these are the recompiled update
patches, not retail content)
**No game files.** Players download the zip, run `skate3`, click "Select
ISO", and the built-in installer extracts their own legally-owned copy. The
game dump (`default.xex` + `data/webkit/EAWebkit.xex`) is required **only at
build time** — codegen mechanically translates the actual Xbox 360 binary
into C++ source, which is what gets compiled. It is never included in, or
uploaded with, a release.
## Option A — build locally, upload manually (recommended)
You already build successfully on your machine, so this is the simple path:
```sh
# on your build machine, from the repo root
./tools/release/release.sh # build + package this platform
./tools/release/release.sh --tag v1.0.0 --upload # build + publish to Forgejo
```
The script builds (configure → codegen → build), packages the zip (binaries
+ `librexruntime.so` + `ghost_relay` + README + TU patches if used), prints a
`sha256sum`, and with `--upload` creates/updates a Forgejo Release and
attaches the zip.
`--upload` needs:
- `GITEA_TOKEN` — a Forgejo token with **write:repository** scope (Settings →
Applications → Generate New Token), covering the release APIs.
- Optional `FORGEJO_REPO` (default `ad3laid3/skate3recomp-multiplayer`) and
`FORGEJO_INSTANCE` (default `https://git.mithraic.cloud`).
Per-platform notes:
| Platform | Build machine | Command |
|---|---|---|
| Linux | any Linux with the toolchain (README: Ubuntu 24.04 + Clang 20) | `./tools/release/release.sh` |
| Windows | a **Windows** machine (MSVC/clang-cl + Windows SDK, D3D12) | run the same script on Windows (`--os windows`) |
| macOS | an **Apple Silicon/Intel Mac** (Homebrew LLVM + MoltenVK, per README) | run the same script on macOS |
Cross-compiling Windows/macOS from a Linux box is not practical for this
stack (D3D12 on Windows, Cocoa/MoltenVK on macOS need their native SDKs), so
each platform's release is built on that platform.
## Option B — automated via Forgejo Actions (optional)
`.forgejo/workflows/release.yml` builds and publishes Linux releases on push
of a `v*` tag (or a manual run), on a self-hosted runner.
Requirements for the automation path:
1. **A runner with the game dump.** Codegen needs the `.xex` files at build
time. On the runner, put the extracted dump at `/opt/skate3-game`
(`default.xex` + `data/webkit/EAWebkit.xex`, optionally the
`TU_12K2276_*` package). If your runner is a Docker container, mount that
directory into it (`-v /opt/skate3-game:/opt/skate3-game`). The game files
stay on the runner — they are never uploaded to the repo.
2. **Secrets** on the repo:
- `SUBMODULE_KEY` — an SSH deploy key with read access to
`ad3laid3/rexglue-skate3` (the self-hosted rexglue fork submodule).
- `RELEASE_TOKEN` — a Forgejo token with `write:repository` scope.
3. **Runner label** — the workflow uses `runs-on: ubuntu-24.04`; match it to
your runner's label (Docker runners commonly use
`ubuntu-latest`/`ubuntu-22.04`/`ubuntu-24.04` — change the `runs-on` line
to match).
4. **Broken submodule pins.** A fresh recursive clone currently fails because
the rexglue fork pins `thirdparty/imgui` to a commit `ocornut/imgui` no
longer carries. `tools/ci/init-submodules.sh` self-heals this (checks out
the upstream default tip for any nested submodule left empty) and the
workflow calls it after `git submodule update --init`.
Windows/macOS jobs can be added to the same workflow when native
Windows/macOS runners are available (each job uses its platform preset and
artifact names; the package step is per-OS).
## Doing it by hand
If you'd rather not use the script, the equivalent steps are:
```sh
cmake --preset linux-relwithdebinfo -DSKATE3_GAME_DATA_ROOT="$PWD/game" # add -DSKATE3_TITLE_UPDATE_PACKAGE=... if used
cmake --build --preset linux-relwithdebinfo --target generate-all --parallel
cmake --preset linux-relwithdebinfo -DSKATE3_GAME_DATA_ROOT="$PWD/game"
cmake --build --preset linux-relwithdebinfo --parallel
# zip out/build/linux-relwithdebinfo/{skate3,librexruntime.so,ghost_relay} + README
```
Then create a Release in the Forgejo web UI (`<repo> → Releases → New
Release`) and attach the zip.

40
tools/ci/init-submodules.sh Executable file
View file

@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Recursive submodule init for the rexglue SDK with self-healing for broken
# upstream pins.
#
# The rexglue fork pins several nested third-party submodules (imgui, ...) to
# commits their upstreams no longer carry, so a plain
# `git submodule update --init --recursive` aborts at the first broken pin
# and leaves every later nested submodule empty (a fresh clone cannot build).
# For each nested submodule still left empty, this script fetches its origin
# and checks out the default-branch tip, which is enough to build.
#
# Run from the repository root after `git submodule update --init` (top
# level). No-op when everything initialized fine.
set -uo pipefail
SDK="third_party/rexglue-sdk"
if [ ! -e "$SDK/.git" ]; then
echo "init-submodules: $SDK is not initialized (run git submodule update --init first)"
exit 1
fi
(
cd "$SDK"
git submodule update --init --recursive --jobs 4 >/tmp/ghost_sub_init.log 2>&1 || true
while IFS= read -r path; do
if [ ! -d "$path" ] || [ -z "$(ls -A "$path" 2>/dev/null)" ]; then
echo "init-submodules: self-healing empty submodule $path"
if (cd "$path" && git fetch --depth 1 origin >/dev/null 2>&1 &&
git checkout -q FETCH_HEAD >/dev/null 2>&1); then
echo " ok: $path -> $(git -C "$path" rev-parse --short HEAD)"
else
echo " WARN: could not self-heal $path (build may fail on this dependency)"
fi
fi
done < <(git config --file .gitmodules --get-regexp '^submodule\..*\.path$' | awk '{print $2}')
)
exit 0

144
tools/release/release.sh Executable file
View file

@ -0,0 +1,144 @@
#!/usr/bin/env bash
# Build a Skate 3 recomp release from this checkout and optionally publish it
# to a Forgejo/Gitea Release.
#
# IMPORTANT: the release zip contains ONLY the recompiled binaries
# (skate3 + librexruntime.so + ghost_relay + README) - NO game files. Players
# bring their own ISO and the in-app installer extracts it. The game dump
# (default.xex + data/webkit/EAWebkit.xex) is required at BUILD TIME only,
# because codegen translates the actual Xbox 360 binary into C++ source; it
# is never included in, or uploaded with, the release.
#
# Usage:
# ./tools/release/release.sh # build this platform, no upload
# ./tools/release/release.sh --tag v1.0.0 --upload # build + publish
# ./tools/release/release.sh --game-root /path/to/game --tu /path/to/TU_12K2276_...
#
# Options:
# --tag TAG release tag/version (default: last git tag or 'dev')
# --game-root DIR extracted game dump (default: $REPO/game)
# --tu FILE optional Title Update STFS package for codegen
# --os linux|windows|macos override platform (default: host OS)
# --upload publish the zip to a Forgejo Release
#
# Env for --upload:
# GITEA_TOKEN Forgejo token with write:repository (release) scope
# FORGEJO_REPO default "ad3laid3/skate3recomp-multiplayer"
# FORGEJO_INSTANCE default "https://git.mithraic.cloud"
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$REPO"
UPLOAD=0
TAG=""
GAME_ROOT="${SKATE3_GAME_DATA_ROOT:-$REPO/game}"
TU="${SKATE3_TITLE_UPDATE_PACKAGE:-}"
OS_ARG=""
while [ $# -gt 0 ]; do
case "$1" in
--tag) TAG="${2:?--tag needs a value}"; shift 2;;
--game-root) GAME_ROOT="$2"; shift 2;;
--tu) TU="$2"; shift 2;;
--os) OS_ARG="$2"; shift 2;;
--upload) UPLOAD=1; shift;;
-h|--help)
sed -n '2,30p' "$0"; exit 0;;
*) echo "unknown argument: $1"; exit 2;;
esac
done
# ---- platform ---------------------------------------------------------------
UNAME_S="$(uname -s 2>/dev/null || echo Windows)"
case "${OS_ARG:-$UNAME_S}" in
linux|Linux) PRESET="linux-relwithdebinfo"; BIN="skate3"; LIB="librexruntime.so"; EXT="Linux";;
windows|Windows|MINGW*|MSYS*|CYGWIN*)
PRESET="relwithdebinfo"; BIN="skate3.exe"; LIB="librexruntime.dll"; EXT="Windows";;
macos|macOS|Darwin) PRESET="macos-relwithdebinfo"; BIN="skate3"; LIB="librexruntime.dylib"; EXT="macOS";;
*) echo "unsupported platform: ${OS_ARG:-$UNAME_S} (set --os linux|windows|macos)"; exit 2;;
esac
if [ -z "$TAG" ]; then
TAG="$(git describe --tags --always 2>/dev/null || echo dev)"
fi
VER="${TAG#v}"
NAME="Skate3Recomp-$EXT-$VER"
if [ ! -f "$GAME_ROOT/default.xex" ]; then
echo "ERROR: game dump not found at $GAME_ROOT (need default.xex + data/webkit/EAWebkit.xex)"
echo " It is required for codegen; it is NOT shipped in the release."
exit 1
fi
TU_ARGS=""
if [ -n "$TU" ]; then
if [ ! -f "$TU" ]; then echo "ERROR: title update package not found: $TU"; exit 1; fi
TU_ARGS="-DSKATE3_TITLE_UPDATE_PACKAGE=$TU"
fi
echo "==> Release $TAG ($EXT) preset=$PRESET game=$GAME_ROOT"
echo "==> Configure"
cmake --preset "$PRESET" -DSKATE3_GAME_DATA_ROOT="$GAME_ROOT" $TU_ARGS
echo "==> Generate recompiled code"
cmake --build --preset "$PRESET" --target generate-all --parallel
echo "==> Reconfigure (generated sources)"
cmake --preset "$PRESET" -DSKATE3_GAME_DATA_ROOT="$GAME_ROOT" $TU_ARGS
echo "==> Build"
cmake --build --preset "$PRESET" --parallel
# ---- package ---------------------------------------------------------------
OUT="out/build/$PRESET"
STAGE="$NAME"
rm -rf "$STAGE"; mkdir -p "$STAGE"
cp "$OUT/$BIN" "$STAGE/"
cp "$OUT/$LIB" "$STAGE/" 2>/dev/null || true
cp "$OUT/ghost_relay" "$STAGE/" 2>/dev/null || true
cp "$OUT/ghost_relay.exe" "$STAGE/" 2>/dev/null || true
# Title-update patches staged next to the binary when the build used the TU
if [ -f "$OUT/game/default.xexp" ]; then
mkdir -p "$STAGE/game/data/webkit"
cp "$OUT/game/default.xexp" "$STAGE/game/"
cp "$OUT/game/data/webkit/EAWebkit.xexp" "$STAGE/game/data/webkit/" 2>/dev/null || true
fi
cp README.md "$STAGE/"
rm -f "$NAME.zip"
zip -r "$NAME.zip" "$STAGE"
rm -rf "$STAGE"
echo "==> Packaged: $NAME.zip ($(du -h "$NAME.zip" | cut -f1))"
sha256sum "$NAME.zip"
# ---- publish ----------------------------------------------------------------
if [ "$UPLOAD" = 1 ]; then
: "${GITEA_TOKEN:?--upload needs GITEA_TOKEN}"
REPO_FULL="${FORGEJO_REPO:-ad3laid3/skate3recomp-multiplayer}"
INSTANCE="${FORGEJO_INSTANCE:-https://git.mithraic.cloud}"
AUTH="Authorization: token $GITEA_TOKEN"
echo "==> Publishing $NAME.zip to $INSTANCE/$REPO_FULL release '$TAG'"
RELEASE_ID="$(curl -s -X POST "$INSTANCE/api/v1/repos/$REPO_FULL/releases" \
-H "$AUTH" -H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"Skate 3 Recomp $TAG\",\"target_commitish\":\"main\"}" \
| python3 -c "import sys,json
try: print(json.load(sys.stdin).get('id',''))
except Exception: print('')")"
if [ -z "$RELEASE_ID" ]; then
RELEASE_ID="$(curl -s "$INSTANCE/api/v1/repos/$REPO_FULL/releases/tags/$TAG" -H "$AUTH" \
| python3 -c "import sys,json
try: print(json.load(sys.stdin).get('id',''))
except Exception: print('')")"
fi
if [ -z "$RELEASE_ID" ]; then
echo "ERROR: could not create/find release '$TAG'"; exit 1
fi
curl -s -X POST "$INSTANCE/api/v1/repos/$REPO_FULL/releases/$RELEASE_ID/assets" \
-H "$AUTH" -F "attachment=@$NAME.zip" -F "name=$NAME.zip" \
| python3 -c "import sys,json
try:
a=json.load(sys.stdin); print('Uploaded asset', a.get('name'), a.get('size'), 'bytes')
except Exception as e: print('upload response parse failed:', e)"
echo "==> Done: https://git.mithraic.cloud/$REPO_FULL/releases/tag/$TAG"
fi