From c848e936bbd7610e84a3f76509c29068485fa51a Mon Sep 17 00:00:00 2001 From: Ryan Fraser Date: Wed, 14 Oct 2026 09:43:53 -0500 Subject: [PATCH 1/7] build: make macOS export preset produce a launchable app - Set bundle identifier, version, and export path for the macOS preset - Sign with Xcode codesign instead of Godot's built-in signer; the built-in ad-hoc signature gets the binary SIGKILLed at exec on current macOS (exit 137, launchd error 163) - Ignore local build/ output Co-Authored-By: Claude Opus 5.5 --- .gitignore | 3 +++ export_presets.cfg | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 6696ada6..d5ffc372 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ mono_crash.*.json # Other nuget.config *.lscache + +# Local export output +build/ diff --git a/export_presets.cfg b/export_presets.cfg index d69582c3..f8dd70a5 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -136,7 +136,7 @@ custom_features="" export_filter="all_resources" include_filter="user/*" exclude_filter="" -export_path="" +export_path="build/macOS/Rhythia.zip" patches=PackedStringArray() patch_delta_encoding=false patch_delta_compression_level_zstd=19 @@ -160,11 +160,11 @@ debug/export_console_wrapper=1 application/liquid_glass_icon="" application/icon="" application/icon_interpolation=4 -application/bundle_identifier="" +application/bundle_identifier="io.github.rhythia.client" application/signature="" application/app_category="Games" -application/short_version="" -application/version="" +application/short_version="0.2.0" +application/version="0.2.0" application/copyright="" application/copyright_localized={} application/min_macos_version_x86_64="11.00" @@ -179,7 +179,7 @@ xcode/sdk_build="22C55" xcode/sdk_name="macosx13.1" xcode/xcode_version="1420" xcode/xcode_build="14C18" -codesign/codesign=1 +codesign/codesign=3 codesign/installer_identity="" codesign/apple_team_id="" codesign/identity="" From 46b1114ebb1fdbc2fa4d47002786f2308a2213de Mon Sep 17 00:00:00 2001 From: Ryan Fraser Date: Wed, 14 Oct 2026 09:54:08 -0500 Subject: [PATCH 2/7] build: use upstream macOS bundle identifier The published 0.2.0 macOS.zip ships as wiki.rhythia.net; match it so local and CI exports are the same app to macOS. Co-Authored-By: Claude Opus 5.5 --- export_presets.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/export_presets.cfg b/export_presets.cfg index f8dd70a5..d571c3f6 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -160,7 +160,7 @@ debug/export_console_wrapper=1 application/liquid_glass_icon="" application/icon="" application/icon_interpolation=4 -application/bundle_identifier="io.github.rhythia.client" +application/bundle_identifier="wiki.rhythia.net" application/signature="" application/app_category="Games" application/short_version="0.2.0" From f84a4d0a347f5e0db05e146510f31879e3ea7a9c Mon Sep 17 00:00:00 2001 From: Ryan Fraser Date: Wed, 14 Oct 2026 09:54:08 -0500 Subject: [PATCH 3/7] fix: restart and self-update on macOS and Linux - restartGame passed the relaunch script to /bin/sh without -c, so sh treated it as a filename and the game never came back on macOS/Linux - macOS relaunch resolved Rhythia.app/Contents instead of the .app - Quote paths passed to the shell - Updatum detects .app bundles via AppContext.BaseDirectory, which Godot points at Contents/Resources, so it would sync updates inside the running bundle; swap the .app ourselves on macOS instead Co-Authored-By: Claude Opus 5.5 --- scripts/SettingsManager.cs | 5 ++--- scripts/util/Platform.cs | 20 ++++++++++++++++++++ scripts/util/Platform.cs.uid | 1 + scripts/util/Releases.cs | 30 ++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 scripts/util/Platform.cs create mode 100644 scripts/util/Platform.cs.uid diff --git a/scripts/SettingsManager.cs b/scripts/SettingsManager.cs index dc99e357..6f09bb60 100644 --- a/scripts/SettingsManager.cs +++ b/scripts/SettingsManager.cs @@ -269,11 +269,10 @@ private static void restartGame() OS.CreateProcess("powershell.exe", ["-Command", $"Wait-Process -Id {pid}; Start-Process '{executablePath}'"]); break; case "macOS": - string executablePathMac = Path.Combine(executablePath, "../.."); - OS.CreateProcess("/bin/sh", [$"while kill -0 {pid} 2>/dev/null; do sleep 0.1; done; open {executablePathMac}"]); + OS.CreateProcess("/bin/sh", ["-c", $"{Platform.WaitForExitScript}; open {Platform.ShellQuote(Platform.MacAppBundlePath)}"]); break; case "Linux": - OS.CreateProcess("/bin/sh", [$"while kill -0 {pid} 2>/dev/null; do sleep 0.1; done; exec {executablePath}"]); + OS.CreateProcess("/bin/sh", ["-c", $"{Platform.WaitForExitScript}; exec {Platform.ShellQuote(executablePath)}"]); break; } diff --git a/scripts/util/Platform.cs b/scripts/util/Platform.cs new file mode 100644 index 00000000..74254b97 --- /dev/null +++ b/scripts/util/Platform.cs @@ -0,0 +1,20 @@ +using System.IO; +using Godot; + +public static class Platform +{ + /// + /// Path to the running .app bundle on macOS, the executable lives at Rhythia.app/Contents/MacOS/Rhythia + /// + public static string MacAppBundlePath => Path.GetFullPath(Path.Combine(OS.GetExecutablePath(), "../../..")); + + /// + /// Wraps a value in single quotes so it is passed to /bin/sh as one literal argument + /// + public static string ShellQuote(string value) => $"'{value.Replace("'", "'\\''")}'"; + + /// + /// Shell snippet that blocks until the current process has exited + /// + public static string WaitForExitScript => $"while kill -0 {OS.GetProcessId()} 2>/dev/null; do sleep 0.1; done"; +} diff --git a/scripts/util/Platform.cs.uid b/scripts/util/Platform.cs.uid new file mode 100644 index 00000000..8124b9d7 --- /dev/null +++ b/scripts/util/Platform.cs.uid @@ -0,0 +1 @@ +uid://d0b88m1ycctvg diff --git a/scripts/util/Releases.cs b/scripts/util/Releases.cs index aa0bfce0..df223327 100644 --- a/scripts/util/Releases.cs +++ b/scripts/util/Releases.cs @@ -50,6 +50,36 @@ public static async Task InstallUpdateAsync(UpdatumDownloadedAsset asset) return false; } + if (OS.GetName() == "macOS") + { + return installMacUpdate(asset); + } + return await MANAGER.InstallUpdateAsync(asset); } + + // Updatum finds the .app through AppContext.BaseDirectory, which Godot points at Contents/Resources, + // so it would sync the update inside the running bundle instead of replacing it + private static bool installMacUpdate(UpdatumDownloadedAsset asset) + { + if (asset == null || !asset.FileExists) + { + return false; + } + + string app = Platform.ShellQuote(Platform.MacAppBundlePath); + string zip = Platform.ShellQuote(asset.FilePath); + + string script = + $"{Platform.WaitForExitScript}; " + + $"tmp=$(mktemp -d) && ditto -x -k {zip} \"$tmp\" " + + "&& new=$(find \"$tmp\" -maxdepth 1 -name '*.app' | head -n 1) && [ -n \"$new\" ] " + + $"&& mv {app} \"$tmp/old.app\" && {{ ditto \"$new\" {app} || mv \"$tmp/old.app\" {app}; }}; " + + $"xattr -dr com.apple.quarantine {app}; rm -rf \"$tmp\"; open {app}"; + + OS.CreateProcess("/bin/sh", ["-c", script]); + ((SceneTree)Engine.GetMainLoop()).Quit(); + + return true; + } } From 4f3d7c1868aeddc99e2d196f439c3c1265c7cb67 Mon Sep 17 00:00:00 2001 From: Ryan Fraser Date: Wed, 14 Oct 2026 09:54:08 -0500 Subject: [PATCH 4/7] fix: macOS keyboard conventions - Settings toggle uses Cmd+O on macOS (Ctrl+O elsewhere) - Search box no longer captures keypresses held with Cmd - Drop unused Constants.ROOT_FOLDER (cwd is / when launched from Finder) Co-Authored-By: Claude Opus 5.5 --- scripts/Constants.cs | 2 -- scripts/ui/SettingsMenu.cs | 2 +- scripts/ui/menu/play/SearchPanel.cs | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/Constants.cs b/scripts/Constants.cs index 4c961086..1be1f94d 100644 --- a/scripts/Constants.cs +++ b/scripts/Constants.cs @@ -6,8 +6,6 @@ public partial class Constants : Node { public static readonly ulong STARTED = Time.GetTicksUsec(); - public static readonly string ROOT_FOLDER = Directory.GetCurrentDirectory(); - public static readonly string DEFAULT_USER_FOLDER = OS.GetUserDataDir(); public static readonly string USER_FOLDER_POINTER = $"{DEFAULT_USER_FOLDER}/user_folder_path.txt"; diff --git a/scripts/ui/SettingsMenu.cs b/scripts/ui/SettingsMenu.cs index ddc91b4e..4e18e552 100644 --- a/scripts/ui/SettingsMenu.cs +++ b/scripts/ui/SettingsMenu.cs @@ -225,7 +225,7 @@ public override void _Input(InputEvent @event) switch (eventKey.Keycode) { case Key.O: - if (eventKey.CtrlPressed) + if (eventKey.IsCommandOrControlPressed()) { ShowMenu(!Shown); GetViewport().SetInputAsHandled(); diff --git a/scripts/ui/menu/play/SearchPanel.cs b/scripts/ui/menu/play/SearchPanel.cs index 56949793..25493204 100644 --- a/scripts/ui/menu/play/SearchPanel.cs +++ b/scripts/ui/menu/play/SearchPanel.cs @@ -23,7 +23,7 @@ public override void _Ready() public override void _Input(InputEvent @event) { - if (@event is InputEventKey eventKey && eventKey.Pressed && !eventKey.CtrlPressed && !eventKey.AltPressed) + if (@event is InputEventKey eventKey && eventKey.Pressed && !eventKey.CtrlPressed && !eventKey.MetaPressed && !eventKey.AltPressed) { Control focusOwner = GetViewport().GuiGetFocusOwner(); From bda323805b04fdcb785ac62ca4b10b2603ddd691 Mon Sep 17 00:00:00 2001 From: Ryan Fraser Date: Wed, 14 Oct 2026 10:06:42 -0500 Subject: [PATCH 5/7] ci: build Windows, Linux, and macOS releases in Actions Exports all three presets from the committed export_presets.cfg on release publish (or manual dispatch) and uploads Windows.zip, Linux.zip, and macOS.zip with the same layout as current releases. - macOS exports on a macOS runner since the preset signs with Xcode codesign; the job launches the app to catch signing regressions - Godot version comes from .github/godot_version.txt, same as CI - Fix the opengl3 compatibility launchers: the .sh passed the flag as one quoted argument, the .bat only worked from the game folder - Stop ignoring export_presets.cfg, it is tracked and CI needs it Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 1 + .../release-extras/opengl3-compatibility.bat | 2 + .../release-extras/opengl3-compatibility.sh | 4 + .github/scripts/install-godot.sh | 57 ++++++++++ .github/workflows/release.yml | 100 ++++++++++++++++++ .gitignore | 1 - 6 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 .github/release-extras/opengl3-compatibility.bat create mode 100755 .github/release-extras/opengl3-compatibility.sh create mode 100755 .github/scripts/install-godot.sh create mode 100644 .github/workflows/release.yml diff --git a/.gitattributes b/.gitattributes index 8ad74f78..8c7e7cd4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ # Normalize EOL for all files that Git considers text files. * text=auto eol=lf +*.bat text eol=crlf diff --git a/.github/release-extras/opengl3-compatibility.bat b/.github/release-extras/opengl3-compatibility.bat new file mode 100644 index 00000000..20c2fbc1 --- /dev/null +++ b/.github/release-extras/opengl3-compatibility.bat @@ -0,0 +1,2 @@ +@echo off +"%~dp0Rhythia.exe" --rendering-driver opengl3 %* diff --git a/.github/release-extras/opengl3-compatibility.sh b/.github/release-extras/opengl3-compatibility.sh new file mode 100755 index 00000000..b55cdb2f --- /dev/null +++ b/.github/release-extras/opengl3-compatibility.sh @@ -0,0 +1,4 @@ +#!/bin/sh +printf '\033c\033]0;%s\a' Rhythia +base_path="$(dirname "$(realpath "$0")")" +exec "$base_path/Rhythia.x86_64" --rendering-driver opengl3 "$@" diff --git a/.github/scripts/install-godot.sh b/.github/scripts/install-godot.sh new file mode 100755 index 00000000..ba28728f --- /dev/null +++ b/.github/scripts/install-godot.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Installs the Godot .NET editor and export templates matching .github/godot_version.txt +# Exports GODOT (path to the editor binary) to $GITHUB_ENV when running in Actions +set -euo pipefail + +VERSION=$(tr -d '[:space:]' < .github/godot_version.txt) +TAG="${VERSION}-stable" +BASE_URL="https://github.com/godotengine/godot/releases/download/${TAG}" +CACHE_DIR="${GODOT_CACHE:-$HOME/.cache/godot-downloads}" + +case "$(uname -s)" in + Linux) + EDITOR_ZIP="Godot_v${TAG}_mono_linux_x86_64.zip" + TEMPLATE_DIR="$HOME/.local/share/godot/export_templates/${VERSION}.stable.mono" + GODOT="$CACHE_DIR/Godot_v${TAG}_mono_linux_x86_64/Godot_v${TAG}_mono_linux.x86_64" + ;; + Darwin) + EDITOR_ZIP="Godot_v${TAG}_mono_macos.universal.zip" + TEMPLATE_DIR="$HOME/Library/Application Support/Godot/export_templates/${VERSION}.stable.mono" + GODOT="$CACHE_DIR/Godot_mono.app/Contents/MacOS/Godot" + ;; + *) + echo "Unsupported OS: $(uname -s)" >&2 + exit 1 + ;; +esac + +TEMPLATES_TPZ="Godot_v${TAG}_mono_export_templates.tpz" + +mkdir -p "$CACHE_DIR" + +for file in "$EDITOR_ZIP" "$TEMPLATES_TPZ"; do + if [ ! -f "$CACHE_DIR/$file" ]; then + echo "Downloading $file" + curl -fsSL --retry 3 -o "$CACHE_DIR/$file" "$BASE_URL/$file" + fi +done + +if [ ! -x "$GODOT" ]; then + unzip -q -o "$CACHE_DIR/$EDITOR_ZIP" -d "$CACHE_DIR" +fi + +if [ ! -f "$TEMPLATE_DIR/version.txt" ]; then + rm -rf "$CACHE_DIR/templates" + unzip -q -o "$CACHE_DIR/$TEMPLATES_TPZ" -d "$CACHE_DIR" + mkdir -p "$TEMPLATE_DIR" + cp -R "$CACHE_DIR/templates/." "$TEMPLATE_DIR/" + rm -rf "$CACHE_DIR/templates" +fi + +"$GODOT" --headless --version + +if [ -n "${GITHUB_ENV:-}" ]; then + echo "GODOT=$GODOT" >> "$GITHUB_ENV" +else + echo "GODOT=$GODOT" +fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..bcdf61b3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,100 @@ +name: Release + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: write + +jobs: + export: + name: Export ${{ matrix.preset }} + strategy: + fail-fast: false + matrix: + include: + - preset: Windows Desktop + asset: Windows.zip + runner: ubuntu-latest + extras: opengl3-compatibility.bat + binary: Rhythia.exe + export: Rhythia.zip + - preset: Linux + asset: Linux.zip + runner: ubuntu-latest + extras: opengl3-compatibility.sh + binary: Rhythia.x86_64 + export: Rhythia.x86_64.zip + # macOS must export on a Mac: the preset signs with Xcode codesign, + # Godot's built-in signer produces apps that are killed at launch + - preset: macOS + asset: macOS.zip + runner: macos-latest + extras: "" + binary: Rhythia.app/Contents/MacOS/Rhythia + export: Rhythia.zip + runs-on: ${{ matrix.runner }} + env: + GODOT_CACHE: ${{ github.workspace }}/../godot-cache + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # Checkout v4 + + - name: Setup + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # Dotnet 4.0.2 + with: + dotnet-version: 10.0.x + + - name: Cache Godot + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # Cache v4 + with: + path: ${{ env.GODOT_CACHE }} + key: godot-${{ runner.os }}-${{ hashFiles('.github/godot_version.txt') }} + + - name: Install Godot + run: .github/scripts/install-godot.sh + + - name: Restore dependencies + run: dotnet restore --locked-mode + + - name: Build + run: dotnet build --no-restore + + - name: Import project + run: '"$GODOT" --headless --path . --import' + + # Godot names the executable after the export file, so export under the binary's name and rename + - name: Export + run: | + mkdir -p build/export + "$GODOT" --headless --path . --export-release "${{ matrix.preset }}" "build/export/${{ matrix.export }}" + mv "build/export/${{ matrix.export }}" "build/${{ matrix.asset }}" + + - name: Add launch scripts + if: matrix.extras != '' + working-directory: .github/release-extras + run: zip -X "../../build/${{ matrix.asset }}" ${{ matrix.extras }} + + - name: Verify archive layout + run: unzip -Z1 "build/${{ matrix.asset }}" | grep -x "${{ matrix.binary }}" > /dev/null + + - name: Verify macOS app launches + if: matrix.preset == 'macOS' + run: | + mkdir -p build/verify + ditto -x -k build/macOS.zip build/verify + codesign --verify --deep --strict build/verify/Rhythia.app + build/verify/Rhythia.app/Contents/MacOS/Rhythia --headless --version + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # Upload Artifact v4 + with: + name: ${{ matrix.asset }} + path: build/${{ matrix.asset }} + if-no-files-found: error + + - name: Upload to release + if: github.event_name == 'release' + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload "${{ github.event.release.tag_name }}" "build/${{ matrix.asset }}" --clobber diff --git a/.gitignore b/.gitignore index d5ffc372..610009a8 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,6 @@ Properties/ # Godot 4+ specific ignores .godot/ .import/ -export_presets.cfg # Imported translations (automatically generated from CSV files) *.translation From 737a7abfc9bc4e04e3fcd490fecc5ac388f414a9 Mon Sep 17 00:00:00 2001 From: Ryan Fraser Date: Wed, 14 Oct 2026 10:32:05 -0500 Subject: [PATCH 6/7] docs: macOS install steps and Gatekeeper workaround Builds are ad-hoc signed (no Developer ID), so document Open Anyway and the xattr command for the first launch. Co-Authored-By: Claude Opus 5.5 --- README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/README.md b/README.md index 0b2274a8..003a3679 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ - [Windows](#windows) - [Linux](#linux) - [Linux Troubleshooting](#linux-troubleshooting) + - [macOS](#macos) + - [macOS Troubleshooting](#macos-troubleshooting) - [User Folder](#user-folder) - [Getting Maps](#getting-maps) - [Development](#development) @@ -138,6 +140,41 @@ sudo chmod +x Rhythia.x86_64 Still having issues? Visit the **#support** channel in our [Discord](https://discord.gg/rhythia). +### macOS + +> [!NOTE] +> Requires macOS 11 (Big Sur) or later on Intel, or macOS 13 (Ventura) or later on Apple Silicon. + +1. Download `macOS.zip` for your chosen client above. +2. Double-click the downloaded file to extract `Rhythia.app`. +3. Move `Rhythia.app` to your **Applications** folder. +4. Open `Rhythia.app`. The first launch will be blocked, see below. + +#### Allowing Rhythia to Open + +Rhythia is not signed with an Apple Developer ID, so macOS blocks it the first time you open it. You only need to do this once; in-game updates keep working afterwards. + +**System Settings:** + +1. Try to open `Rhythia.app` and close the warning that appears. +2. Open **System Settings** → **Privacy & Security**. +3. Scroll down to the message about Rhythia being blocked and click **Open Anyway**. +4. Confirm with your password, then open Rhythia again. + +**Terminal:** + +```bash +xattr -dr com.apple.quarantine /Applications/Rhythia.app +``` + +### macOS Troubleshooting + +If macOS says the app **is damaged and can't be opened**, run the Terminal command above; it removes the download flag macOS adds to files from the internet. + +If the game won't open at all, check that you are using a release newer than 0.2.0; the 0.2.0 `macOS.zip` and earlier builds were signed incorrectly and are killed by macOS on launch. + +Still having issues? Visit the **#support** channel in our [Discord](https://discord.gg/rhythia). + --- ## User Folder From 60be186cc082eee10d81afb3fad985ab86887160 Mon Sep 17 00:00:00 2001 From: Ryan Fraser Date: Wed, 14 Oct 2026 10:33:17 -0500 Subject: [PATCH 7/7] docs: add macOS to downloads, user folder, and contributor setup Co-Authored-By: Claude Opus 5.5 --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 003a3679..db9f79d6 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ You can view all releases on the [Rewrite releases page](https://github.com/Rhyt |---|---| | **Windows** | [Download](https://github.com/Rhythia/Client/releases/latest/download/windows.zip) | | **Linux** | [Download](https://github.com/Rhythia/Client/releases/latest/download/linux.zip) | +| **macOS** | [Download](https://github.com/Rhythia/Client/releases/latest/download/macOS.zip) | You can view the Rewrite source code [here](https://github.com/Rhythia/Client). @@ -185,6 +186,7 @@ Your maps, replays, skins, and settings are stored separately from the game inst |---|---| | **Windows** | `%appdata%\Rhythia` | | **Linux** | `~/.local/share/Rhythia` | +| **macOS** | `~/Library/Application Support/Rhythia` | You can also access the user folder from within the game via the **User Folder** button on the top bar. @@ -247,7 +249,7 @@ We ❤️ developers! Contributions are welcome — whether it's bug fixes, feat | Software | Notes | |---|---| | [Godot 4.7 Stable (.NET)](https://godotengine.org/download) | The C# / .NET build is required | -| [Git](https://git-scm.com/downloads) | Windows: Git for Windows · Linux: `git` package | +| [Git](https://git-scm.com/downloads) | Windows: Git for Windows · Linux: `git` package · macOS: `xcode-select --install` or `brew install git` | | [GitHub Account](https://github.com/signup) | Needed to fork and open pull requests | **Optional:** [Visual Studio Code](https://code.visualstudio.com/) with the [godot-tools](https://marketplace.visualstudio.com/items?itemName=geequlim.godot-tools) extension. @@ -287,6 +289,17 @@ dotnet tool restore 3. Browse to your cloned repository folder and select `project.godot`. 4. Click **Open**. +#### 4. Exporting a Build (Optional) + +Release builds are exported automatically by the [Release workflow](.github/workflows/release.yml) when a release is published. To export locally, install the matching [export templates](https://godotengine.org/download) and use **Project** → **Export**, or: + +```bash +godot --headless --export-release "macOS" build/macOS/Rhythia.zip +``` + +> [!NOTE] +> The macOS preset signs with Apple's `codesign`, so macOS builds must be exported on a Mac. + ### Submitting Your Changes 1. **Format** the project: