Aether is one app, one repo, one main — but it ships to several platforms,
each of which can release on its own cadence. This document is the source of
truth for how a build reaches TestFlight.
staging— the working base. Every feature/fix branches from it and PRs back into it; CI (AetherCore build,App build + AetherTests,Build (tvOS),Build (visionOS)) must be green before merge.main— the release branch. Mergingstaging → mainis what produces a TestFlight build, so it's done deliberately, one promotion at a time.
Never make per-platform branches — platform differences live in code
(#if os(…) and platform-specific files), not in branches.
Each Xcode Cloud workflow archives exactly one platform (set in its Archive action). To let platforms ship independently — and to avoid App Store Connect rejecting two simultaneous deliveries — the workflows have different start conditions:
| Platform | Workflow start condition | Builds when |
|---|---|---|
| iOS | Tag Changes → ios/… |
an ios/… tag is pushed |
| tvOS | Tag Changes → tvos/… |
a tvos/… tag is pushed |
| visionOS | Tag Changes → visionos/… |
a visionos/… tag is pushed |
| macOS | Tag Changes → macos/… |
a macos/… tag is pushed (workflow must be created first — see below) |
Every platform is tag-gated — nothing auto-builds on a plain push to main.
Merging staging → main means "ready"; pushing the tags means "ship". This
keeps all platforms building the same commit and avoids App Store Connect
rejecting simultaneous deliveries.
-
Promote the release: merge
staging → main(CI green first). This does not trigger any build by itself. -
Tag the platforms so they build the same commit:
scripts/ship-platforms.sh # tags origin/main for iOS + tvOS + visionOSThis reads
MARKETING_VERSIONfromproject.yml, takes the short SHA oforigin/main, and pushes one tag per platform.
That's it — all platforms build the same commit from the tags.
To validate one platform before the others, push its tag by hand, then run the script for the remainder (it skips tags that already exist):
SHA=$(git rev-parse --short origin/main)
VER=$(git show origin/main:project.yml | grep -m1 MARKETING_VERSION: | sed -E 's/.*"([^"]+)".*/\1/')
git tag "ios/$VER-$SHA" origin/main && git push origin "ios/$VER-$SHA" # iOS first
# …verify iOS on TestFlight, then ship the rest:
scripts/ship-platforms.sh # tvOS, visionOS, (macOS)macOS does NOT ship here — see RELEASING-macos.md
macOS is not distributed through Xcode Cloud / the App Store. The player bundles libmpv + FFmpeg (GPL), which the Mac App Store can't host (same reason VLC/IINA aren't there), and the Cloud archive signs ad-hoc (can't carry the app's entitlements). So macOS ships as a Developer ID-signed, notarized DMG built locally and downloaded from the website:
scripts/package-mac.sh # build → sign (Developer ID) → notarize → staple → DMG
scripts/deploy-dmg.sh # upload the DMG to the website + verifyFull setup + steps: RELEASING-macos.md. A macos/… tag
and Cloud workflow may exist, but they're not the distribution path.
<platform>/<MARKETING_VERSION>-<short-sha> e.g. tvos/0.6.4-b436e24
- Unique per build —
MARKETING_VERSIONstays on a value (e.g.0.6.4) across many builds, so the short SHA makes each tag distinct; otherwise re-pushing the same tag wouldn't trigger a new Xcode Cloud build. - Traceable — every platform build maps back to an exact commit + version.
CFBundleVersion is stamped at build time by ci_scripts/ci_pre_xcodebuild.sh
from CI_BUILD_NUMBER (globally unique per build across the team), so each
platform's TestFlight track gets unique, non-colliding build numbers
automatically. MARKETING_VERSION in project.yml is the shared, human
version and is bumped by hand (patch bumps are routine; minor/major are a
deliberate call).
- App Store Connect processes one delivery per app at a time. If two
platform archives finish and deliver simultaneously, the second fails with
"An update has already been initiated by another request…" — just Rebuild
it once the first clears "Processing".
ship-platforms.shpushes the tags back-to-back, which usually staggers delivery enough; if you still hit it, Rebuild the loser. - Nothing builds on a plain push/merge to
main— every platform is tag-gated, so docs/CI-only changes never spend a build. Builds happen only when you push theios/…tvos/…visionos/…tags (i.e. run the script). ci_scripts/ci_post_clone.shruns for every workflow (fetches VLCKit + libmpv, writes secrets from theTMDB_API_KEYenv var). It can branch onCI_PRODUCT_PLATFORM/CI_WORKFLOWif a platform ever needs different setup.- macOS is Apple Silicon only (
ARCHS = arm64) — Homebrew's libmpv has no x86_64 slice. An Intel Mac build would need a separately-built libmpv.