Use when exporting, packaging, signing, or publishing a game — export presets per engine, macOS notarization, cross-origin isolation for threaded web builds, Android AAB/keystore, iOS provisioning, store submission (Steam, Play, App Store, itch.io), headless build CI. NOT gameplay code inside the artifact (that is `godot`, `unity`, `unreal`), NOT hosting a web app or container (that is `deployment`).
npx skills add https://github.com/ericrisco/rsc-harness --skill gamedev-shipping
Take a finished project from editor → signed, correctly-configured artifact → live on the
right target (desktop / web / mobile / store), with the platform gotchas that silently break a
build handled up front. This skill owns the *packaging and release boundary*; the engine skills
own the code that runs inside the artifact.
project ──[engine export/build]──▶ artifact ──[per-platform sign/config]──▶ target
Godot templates·Unity Build Profiles·Unreal Shipping desktop·web·mobile Steam·Play·App Store·itch
| Tool | Target | Ship note |
| --- | --- | --- |
| Godot | 4.5 stable (4.6 dev) | Export templates must match the editor build exactly (4.5.1 editor ⇒ 4.5.1 templates). C#/.NET edition has NO web export — GDScript/standard only. |
| Unity | Unity 6 (6000.x) | "Build Settings" is now Build Profiles. IL2CPP backend for iOS/Android/WebGL. |
| Unreal | 5.6 | Ship in the Shipping config; test packaged builds in Development. |
| Android | JDK 17, target a recent API level | AAB required for Google Play (new apps + updates). APK only for sideload/itch/direct. |
| iOS/macOS | Xcode 16+, notarytool | altool notarization is dead — use xcrun notarytool. |
| Ask | Owner | Why the line is there |
| --- | --- | --- |
| Gameplay, shaders, netcode, physics — any runtime logic | ../godot/SKILL.md, ../unity/SKILL.md, ../unreal/SKILL.md, ../gamedev-shaders/SKILL.md, ../gamedev-multiplayer/SKILL.md, ../gamedev-physics/SKILL.md | This skill packages code, it does not write it. The boundary is the export button. |
| Hosting a normal web app, API or container (Dockerfile, Coolify, Vercel, VPS) | ../deployment/SKILL.md | A WebAssembly game is *static files*; only its headers are special (below). Borrow its Actions + header-setting patterns. |
| Game feel, economy, narrative | ../game-design/SKILL.md, ../game-storytelling/SKILL.md | Design intent, not packaging. |
| Signing-key and CI secret handling in general | ../secure-coding/SKILL.md | The practice this skill assumes for keystores/certs. |
| Question | Rule |
| --- | --- |
| Web build uses threads / SharedArrayBuffer? | You must serve COOP: same-origin + COEP: require-corp (cross-origin isolation). No headers ⇒ no threads ⇒ boot failure. |
| Godot + C# + web? | Impossible today. Switch to GDScript for web, or ship that title desktop/mobile only. |
| Play Store? | AAB, signed with an upload key, Play App Signing on. Not APK. |
| macOS distribution outside the App Store? | codesign (Developer ID) → notarize → staple. Skipping = Gatekeeper blocks it. |
| Mac App Store? | Different cert (Apple Distribution) + sandbox entitlements; notarization is automatic, not manual. |
| Steam? | SteamPipe upload via steamcmd + an app_build VDF; set the default branch live in the partner site. |
| Quick public build / demo / jam? | itch.io + butler push. Tick "SharedArrayBuffer support" for threaded web builds. |
| Release config? | Godot release preset (debug off), Unity release/IL2CPP, Unreal Shipping. Never ship a debug/Development build. |
Godot — Editor ▸ Manage Export Templates (match version), then Project ▸ Export, add one
preset per platform. Headless for CI:
godot --headless --export-release "Windows Desktop" build/game.exe
godot --headless --export-release "Web" build/index.html
# Android AAB needs the Gradle build template + JDK 17 + a release keystore (see references)
Web preset toggles: Thread Support (⇒ needs COOP/COEP) and Extensions Support. Android
needs Project ▸ Install Android Build Template (enables *Use Gradle Build*).
Unity — File ▸ Build Profiles (formerly Build Settings). Create a profile per platform;
override Player Settings per profile (company/product name, bundle id, icons, scripting
backend). iOS/Android/WebGL force IL2CPP. CI headless:
Unity -quit -batchmode -nographics -projectPath . \
-buildTarget Android -executeMethod BuildScript.PerformBuild -logFile -
Android: tick Build App Bundle (Google Play) for AAB; set the keystore in *Publishing Settings*.
WebGL: threads ⇒ COOP/COEP; keep decompression fallback for hosts that can't set gzip/br headers.
Unreal — build configurations, not "modes": Debug, DebugGame, Development (test packaged
builds — keeps logs/console), Test, Shipping (release — strips logging, most optimized). Package
via Platforms ▸ <target> ▸ Package Project, the Project Launcher, or UAT for CI:
RunUAT BuildCookRun -project="Game.uproject" -platform=Win64 \
-clientconfig=Shipping -cook -stage -pak -package -archivedirectory=build
Android adds -distribution for an AAB; iOS requires a Mac + provisioning. Run the platform
Turnkey/SDK setup once (Android Studio + NDK, or Xcode) or the cook fails.
→ full per-engine preset/flag tables: references/mobile-and-desktop.md
and references/stores-and-ci.md.
| OS | Sign? | Gotcha |
| --- | --- | --- |
| Windows | Optional (Authenticode; EV avoids SmartScreen warm-up) | Unsigned still runs but SmartScreen warns; ship .exe + engine data next to it, or an installer. |
| Linux | No | Ship binary + data, or wrap as AppImage/Flatpak for a portable single file. |
| macOS | Required | Must codesign (Developer ID Application) + notarize + staple, or Gatekeeper refuses to open it. |
macOS notarization (the step everyone forgets):
codesign --deep --force --timestamp --options runtime \
--sign "Developer ID Application: Studio (TEAMID)" Game.app
ditto -c -k --keepParent Game.app Game.zip
xcrun notarytool submit Game.zip --keychain-profile "AC" --wait # store creds once via store-credentials
xcrun stapler staple Game.app # staple so it verifies offline
--options runtime (Hardened Runtime) is mandatory for notarization. Codesign every bundled
.dylib/helper *before* the outer .app, inside-out.
Threaded WebAssembly builds (Godot with Thread Support, Unity WebGL with threads) use
SharedArrayBuffer, which browsers gate behind cross-origin isolation. The server must send:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Missing them ⇒ SharedArrayBuffer is not defined, a hang, or a blank canvas — even though the
files are fine. Where to set them:
_headers, Vercel vercel.json headers, nginx add_header — set both on the game path.coi-serviceworker shim (a service worker that re-serves with the headers).COEP: require-corp also means every cross-origin subresource (fonts, analytics, embeds) must
send CORP/CORS or it's blocked. Godot C#/.NET has no web export at all — use GDScript.
→ per-host configs, the service-worker shim, COEP subresource fallout, a self-test snippet and the
Godot vs Unity web specifics: references/web-export-headers.md.
Android — needs JDK 17 and the engine's Gradle build template. Ship AAB to Play
(Google re-signs and generates per-device APKs), APK for sideload/itch/direct. Generate a
release keystore once and reuse it forever (lose it = you can't update the app without Play App
Signing key reset):
keytool -genkey -v -keystore release.keystore -alias game \
-keyalg RSA -keysize 2048 -validity 10000
Keep the keystore + passwords out of git (CI secret). versionCode must increase every upload.
iOS — Mac + Xcode + Apple Developer account. Bundle ID must match an App ID; pick a
provisioning profile (development / ad-hoc / App Store) and a signing certificate. Archive,
then upload to App Store Connect / TestFlight via Xcode Organizer or Transporter. CFBundleVersion
must increase per upload.
→ full keystore + Play App Signing, provisioning/TestFlight, Windows Authenticode and Linux
AppImage/Flatpak walkthroughs: references/mobile-and-desktop.md.
.ico, macOS .icns), splash/launch, store screenshots per device class, a trailer where required.steamcmd +run_app_build app_build_APPID.vdf); store page assets; set the branch live.butler push build/ user/game:channel --userversion 1.2.3 — channel name (e.g. windows, html5) picks the platform.→ Steamworks/SteamPipe VDF, butler channels, Play/App Store submission checklists:
references/stores-and-ci.md.
Headless export in GitHub Actions; version from the run/commit so every artifact is traceable.
barichello/godot-ci-style image), godot --headless --export-release.game-ci/unity-builder + a license activation step (secrets); matrix over targets.BuildCookRun.macos-latest runner with notarytool + an App Store Connect API key.github.run_number or the git commit count feeds versionCode/CFBundleVersion/Steam build description.→ ready per-engine workflows with signing: references/stores-and-ci.md.
.pck/data, use Ogg Vorbis over WAV. Web: enable gzip/brotli on .wasm/.pck.| Anti-pattern | Do instead |
| --- | --- |
| Web build works locally, host it later | Set COOP/COEP now; file:// and simple servers hide the SharedArrayBuffer failure. |
| Planning a Godot C# web export | It doesn't exist in 4.x. GDScript for web, or no web. |
| Treating notarization as optional | On macOS, no notarize + staple = Gatekeeper blocks the app for every user. |
| Shipping an APK to the Play Store | Play requires AAB for new apps and updates. |
| Committing the keystore so CI can sign | Keystore/cert live in encrypted CI secrets, never in git. |
| Shipping the Development/Debug config | Ship Godot release / Unity IL2CPP release / Unreal Shipping — debug leaks console + is slow. |
| Export templates "close enough" (4.5 vs 4.5.1) | Godot templates must match the editor build exactly or export breaks. |
| Uncompressed textures | Per-platform compression (ASTC/BCn/ETC2) — raw textures blow memory + size. |
| Reusing an old versionCode/CFBundleVersion | Both must strictly increase or the store rejects the upload. |
same-origin + COEP require-corp served; boots without SharedArrayBuffer errors.versionCode bumped.CFBundleVersion bumped.Take ericrisco/gamedev-shipping from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.