Files
Legend-of-Zelda--The--A-Lin…/.github/workflows/release.yml
T

376 lines
14 KiB
YAML

# Build the Legend of Zelda: A Link to the Past SETUP PACK for Linux, Windows, macOS
# arm64 and macOS Intel, tag the commit, and publish the four zips as a
# GitHub Release.
#
# HUMAN-TRIGGERED ONLY. There is no push, pull_request or schedule trigger and
# there must never be one: a run is a release decision, made by a person on
# the Actions page. Releases across the port set are driven from the studio
# bulk tool, and a per-push run would compete with it for minutes and for the
# meaning of "did CI pass".
#
# WHAT A RUN DOES, with the defaults:
# 1. Picks the next version: the latest v* tag with its patch number bumped
# (or VERSION when there is no tag yet, or whatever `version` says).
# 2. Builds the setup host for all four platforms with that version as the
# lobby pin, and packages it.
# 3. Tags the commit the run was started from as vX.Y.Z and publishes a
# public GitHub Release with the four zips. Uncheck publish_release for a
# dry run that only uploads workflow artifacts.
#
# WHAT THIS BUILDS. Not the game. src/gen/*.c is recompiler output derived
# from ROM bytes: never committed, never in CI, never in a zip. What CI can
# honestly produce is the SETUP HOST -- the same executable built with
# -DSNESRECOMP_SETUP_HOST=ON, i.e. without recompiled code -- packaged with
# the recompiler and this source tree. On the player's machine the launcher
# takes their own ROM, generates, rebuilds, and relaunches into the real game.
#
# THE TOOLCHAIN. Each pack is built with the retcomm-toolchains cmake-clang-v1
# pack, because that is what the player's own rebuild uses: if it cannot build
# this game, CI is where that must fail. By default the pack is embedded in
# the zip (embed_toolchain) so first run downloads nothing; turn it off for a
# lean zip that fetches the pack on demand (Linux pack ~800 MB, Windows
# ~200 MB, macOS ~90 MB).
name: Release
on:
workflow_dispatch:
inputs:
publish_release:
description: 'Tag this commit and publish a public GitHub Release'
type: boolean
default: true
version:
description: 'Version to release, e.g. 0.2.0 (empty = bump the latest v* tag)'
type: string
default: ''
bump:
description: 'When version is empty, which part of the latest tag to bump'
type: choice
options: [patch, minor, major]
default: patch
embed_toolchain:
description: 'Embed the cmake-clang-v1 pack in each zip (else the launcher downloads it)'
type: boolean
default: true
toolchain_tag:
description: 'retcomm-toolchains release to build with (empty = latest)'
type: string
default: ''
verify_pins:
description: 'Fail if the submodules disagree with framework_pins.txt'
type: boolean
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
EXE_NAME: LegendOfZeldaALinkToThePastSNESRecomp
ZIP_PREFIX: legendofzeldaalinktothepast
jobs:
preflight:
name: Preflight
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.meta.outputs.version }}
tag: ${{ steps.meta.outputs.tag }}
publish: ${{ steps.meta.outputs.publish }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
fetch-tags: true
- name: Record framework pins
run: |
set -euo pipefail
ARGS=()
if [[ "${{ inputs.verify_pins }}" == "true" ]]; then
ARGS+=(--check framework_pins.txt)
fi
bash snesrecomp/tools/ci/record_pins.sh "${ARGS[@]}"
- name: Resolve release version
id: meta
env:
IN_VERSION: ${{ inputs.version }}
IN_BUMP: ${{ inputs.bump || 'patch' }}
IN_PUBLISH: ${{ inputs.publish_release }}
run: |
set -euo pipefail
PUBLISH=true
[[ "${IN_PUBLISH}" == "false" ]] && PUBLISH=false
FILE_VER="$(tr -d '[:space:]' < VERSION)"
RAW="$(printf '%s' "${IN_VERSION}" | tr -d '[:space:]')"
LATEST="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n1 || true)"
if [[ -n "${RAW}" ]]; then
VER="${RAW#v}"
echo "version given: ${VER}"
elif [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
# Started from an existing tag: release exactly that.
VER="${GITHUB_REF_NAME#v}"
echo "running on tag ${GITHUB_REF_NAME}: releasing ${VER}"
elif [[ -z "${LATEST}" ]]; then
# First release: VERSION says what it is.
VER="${FILE_VER}"
echo "no v* tag yet: first release is VERSION = ${VER}"
else
BASE="${LATEST#v}"; BASE="${BASE%%[-+]*}"
IFS=. read -r MA MI PA <<<"${BASE}"
case "${IN_BUMP}" in
major) MA=$((MA + 1)); MI=0; PA=0 ;;
minor) MI=$((MI + 1)); PA=0 ;;
*) PA=$((PA + 1)) ;;
esac
VER="${MA}.${MI}.${PA}"
# A deliberate bump of VERSION in the tree outranks the automatic
# patch step, so a maintainer can move to 0.2.0 by editing the file.
if [[ "$(printf '%s\n%s\n' "${FILE_VER}" "${VER}" | sort -V | tail -n1)" == "${FILE_VER}" \
&& "${FILE_VER}" != "${BASE}" ]]; then
echo "VERSION (${FILE_VER}) is ahead of ${LATEST} bumped (${VER}); using VERSION"
VER="${FILE_VER}"
else
echo "auto-bump ${IN_BUMP}: ${LATEST} -> ${VER}"
fi
fi
if [[ ! "${VER}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.+-][A-Za-z0-9.+-]*)?$ ]]; then
echo "::error::'${VER}' is not a version (want X.Y.Z)"
exit 1
fi
TAG="v${VER}"
if [[ "${PUBLISH}" == "true" && "${GITHUB_REF}" != "refs/tags/${TAG}" ]] \
&& git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
echo "::error::tag ${TAG} already exists at $(git rev-parse --short "${TAG}"). Pass a new version, or start the run from that tag to re-release it."
exit 1
fi
echo "version=${VER}" >>"$GITHUB_OUTPUT"
echo "tag=${TAG}" >>"$GITHUB_OUTPUT"
echo "publish=${PUBLISH}" >>"$GITHUB_OUTPUT"
echo "release ${TAG} from ${GITHUB_SHA::12}; publish=${PUBLISH}"
build:
name: ${{ matrix.artifact }}
needs: preflight
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
artifact: linux-x64
- os: windows-2022
artifact: windows-x64
- os: macos-15
artifact: macos-arm64
- os: macos-15-intel
artifact: macos-x64
defaults:
run:
shell: bash
env:
# The lobby pin compiled into the binary and written into the pack's
# VERSION, so the tag, the binary and the player's own rebuild agree.
RELEASE_VERSION: ${{ needs.preflight.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 1
- name: Record framework pins
run: bash snesrecomp/tools/ci/record_pins.sh
# A setup host is defined by what it lacks. The checkout must carry no
# generated C; if it ever does, that is committed ROM-derived output and
# the build must stop here rather than package it.
- name: Assert no generated C in the tree
run: |
set -euo pipefail
if compgen -G 'src/gen/*.c' >/dev/null || [[ -f recomp/funcs.h ]]; then
echo "::error::src/gen/*.c or recomp/funcs.h is present in the checkout."
exit 1
fi
echo "src/gen is empty — building the setup host."
- name: Cache toolchain pack
uses: actions/cache@v4
with:
path: .cache/toolchain-pack
key: cmake-clang-v1-${{ matrix.artifact }}-${{ inputs.toolchain_tag || 'latest' }}
- name: Fetch toolchain pack
run: |
set -euo pipefail
ARGS=(--artifact "${{ matrix.artifact }}")
if [[ -n "${{ inputs.toolchain_tag }}" ]]; then
ARGS+=(--tag "${{ inputs.toolchain_tag }}")
fi
bash snesrecomp/tools/ci/fetch_toolchain.sh "${ARGS[@]}"
# The macOS pack is cmake + ninja + python; SDL3 comes from
# runner.cmake's pinned source fallback. Prefetch the tarball with curl
# (HTTP/1.1) and cache it, so configure does not download.
- name: Cache SDL3 source (macOS)
if: runner.os == 'macOS'
uses: actions/cache@v4
with:
path: .cache/sdl3-src
key: sdl3-src-3.4.10
- name: Prefetch SDL3 source (macOS)
if: runner.os == 'macOS'
run: bash snesrecomp/tools/ci/prefetch_sdl3.sh --out-dir .cache/sdl3-src
- name: Configure and build the setup host
run: |
set -euo pipefail
# The pack's env.sh is its own statement of how to use it (PATH,
# CC/CXX, sysroot, SDL3_DIR). Source it rather than restate it.
# shellcheck disable=SC1091
source "${TOOLCHAIN_DIR}/env.sh"
echo "cmake: $(command -v cmake) ($(cmake --version | head -n1))"
echo "cc: ${CC:-$(command -v cc)}"
EXTRA=()
if [[ -n "${SNESRECOMP_SDL3_SOURCE_DIR:-}" ]]; then
EXTRA+=("-DSNESRECOMP_SDL3_SOURCE_DIR=${SNESRECOMP_SDL3_SOURCE_DIR}")
fi
if [[ "${{ runner.os }}" == "macOS" ]]; then
EXTRA+=("-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0")
fi
JOBS="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)"
cmake -S . -B build-ci -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DSNESRECOMP_SETUP_HOST=ON \
-DSNESRECOMP_ENABLE_TRACE=OFF \
-DSNESRECOMP_BUILD_VERSION="${RELEASE_VERSION}" \
"${EXTRA[@]+"${EXTRA[@]}"}"
cmake --build build-ci -j"${JOBS}" --target "${EXE_NAME}"
- name: Package setup pack
run: |
set -euo pipefail
# The packager inspects the executable's imports and zips the
# stage; Git Bash on Windows has neither tool, the pack does.
# shellcheck disable=SC1091
source "${TOOLCHAIN_DIR}/env.sh"
ARGS=()
if [[ "${{ inputs.embed_toolchain }}" != "false" ]]; then
ARGS+=(--embed-toolchain)
fi
bash scripts/package_release.sh build-ci "${{ matrix.artifact }}" "${ARGS[@]+"${ARGS[@]}"}"
ls -la dist/
- uses: actions/upload-artifact@v4
with:
name: ${{ env.ZIP_PREFIX }}-${{ matrix.artifact }}
path: dist/${{ env.ZIP_PREFIX }}-*-${{ matrix.artifact }}.zip
if-no-files-found: error
retention-days: 7
compression-level: 0
release:
name: Tag and publish
needs: [preflight, build]
if: needs.preflight.outputs.publish == 'true'
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
fetch-tags: true
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Collect zips
run: |
set -euo pipefail
mkdir -p dist
find artifacts -type f -name "${ZIP_PREFIX}-*.zip" -exec cp -v {} dist/ \;
# Four platforms in, four zips out. A release that silently carries
# three is the failure this counts for.
count="$(find dist -name "${ZIP_PREFIX}-*.zip" | wc -l | tr -d ' ')"
if [[ "${count}" -ne 4 ]]; then
echo "::error::expected 4 platform zips, found ${count}"
ls -la dist
exit 1
fi
( cd dist && sha256sum "${ZIP_PREFIX}"-*.zip > SHA256SUMS )
cat dist/SHA256SUMS
# The tag is created here, after all four builds succeeded, so a failed
# build never leaves a version number claimed by nothing.
- name: Tag the commit
env:
TAG: ${{ needs.preflight.outputs.tag }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
echo "tag ${TAG} already exists at $(git rev-parse --short "${TAG}") (re-release from that tag)"
else
git tag -a "${TAG}" -m "Legend of Zelda: A Link to the Past ${TAG}" "${GITHUB_SHA}"
git push origin "refs/tags/${TAG}"
echo "tagged ${GITHUB_SHA::12} as ${TAG}"
fi
- name: Release notes
env:
TAG: ${{ needs.preflight.outputs.tag }}
run: |
set -euo pipefail
# From rom_identity.txt, not a copy baked into this workflow.
ROM_FILE="$(python3 snesrecomp/tools/rom_identity.py rom_identity.txt --get rom_file)"
ROM_SHA256="$(python3 snesrecomp/tools/rom_identity.py rom_identity.txt --get expected_sha256)"
{
echo "Legend of Zelda: A Link to the Past ${TAG} — setup packs"
echo
echo "These packs do not contain the game. Each holds a setup program, the"
echo "recompiler and the source; on first run it takes your own copy of"
echo "${ROM_FILE} (SHA-256 ${ROM_SHA256}), recompiles it locally, and builds"
echo "the playable game. No ROM data is included or downloaded."
echo
if [[ "${{ inputs.embed_toolchain }}" != "false" ]]; then
echo "The portable build tools (cmake-clang-v1) are embedded in every pack."
else
echo "The portable build tools (cmake-clang-v1) are downloaded on first run."
fi
echo
echo '### Framework revisions'
echo
echo '```'
bash snesrecomp/tools/ci/record_pins.sh
echo '```'
} > notes.md
cat notes.md
- name: Publish release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.preflight.outputs.tag }}
target_commitish: ${{ github.sha }}
name: "Legend of Zelda: A Link to the Past ${{ needs.preflight.outputs.tag }}"
body_path: notes.md
files: |
dist/${{ env.ZIP_PREFIX }}-*.zip
dist/SHA256SUMS
fail_on_unmatched_files: true
make_latest: true