Early Beta — Shipper is functional but under active development. Expect rough edges. Feedback and bug reports are welcome via GitHub Issues.
Deploy iOS and Android apps to the App Store and Play Store from your Mac — with a single command.
No EAS. No Fastlane. No GitHub Actions. No cloud build services. No Ruby. No YAML.
shipper deploy ios # Build → Sign → TestFlight
shipper deploy android # Build → Sign → Play Store
shipper deploy all # Both, sequentiallyExpo-aware: detects app.json and runs expo prebuild automatically.
· * · *
╱▲╲
│APP│ shipper 0.1.5
│───│ ship it.
╰─┬─╯
│
╱│╲
· · ·
If you've ever thought:
- "Fastlane takes an hour to set up and breaks every time Ruby updates"
- "EAS Submit is another monthly bill just to upload a binary"
- "I don't want to manage GitHub Actions secrets just to push to TestFlight"
Shipper is a single self-contained binary. Install it with Homebrew and deploy in minutes.
| Tool | The problem |
|---|---|
| EAS Submit / EAS Build | Paid cloud service, build credits, queue times |
| Fastlane | Ruby dependency hell, Gemfile maintenance, slow startup |
| GitHub Actions | YAML complexity, secrets sprawl, runner minutes |
| Bitrise / App Center | Expensive, vendor lock-in |
| Shipper | Single binary, runs on your Mac, zero cloud dependencies |
brew tap alcnsahin/tap
brew update && brew upgrade shipperDownload the binary for your platform from the latest release:
| Platform | Binary |
|---|---|
| macOS Apple Silicon (M1/M2/M3/M4) | shipper-macos-arm64 |
| macOS Intel | shipper-macos-x86_64 |
| Linux x86_64 | shipper-linux-x86_64 |
| Windows x86_64 | shipper-windows-x86_64.exe |
# macOS Apple Silicon
curl -Lo shipper https://github.com/alcnsahin/shipper/releases/latest/download/shipper-macos-arm64
chmod +x shipper
sudo mv shipper /usr/local/bin/git clone https://github.com/alcnsahin/shipper
cd shipper
cargo build --release
sudo mv target/release/shipper /usr/local/bin/Requires Rust 1.75+. Install via rustup.rs.
# 1. Initialize in your project root
cd your-app/
shipper init
# 2. Edit credentials (one-time setup)
nano ~/.shipper/config.toml
# 3. Ship
shipper deploy iosInteractive setup that generates shipper.toml in your project root.
For Expo and React Native projects, init reads app.json and eas.json and pre-fills:
- Bundle ID / Package name
- iOS scheme and workspace path
- App Store Connect App ID
- Apple Team ID
- Google service account path
- Android keystore alias
[global]
notify = ["telegram"]
log_level = "info"
[credentials.apple]
team_id = "QC686RQ858"
key_id = "W54D6Z8Y5M"
issuer_id = "your-issuer-id"
key_path = "~/.shipper/keys/AuthKey_W54D6Z8Y5M.p8"
[credentials.google]
service_account = "~/.shipper/keys/play-store-sa.json"
[notifications.telegram]
bot_token_path = "~/.shipper/keys/telegram-bot-token"
chat_id = "-100xxxxxxxxxx"[project]
name = "MyApp"
[ios]
workspace = "ios/MyApp.xcworkspace"
scheme = "MyApp"
bundle_id = "com.company.myapp"
asc_app_id = "1234567890"
export_method = "app-store"
[android]
project_dir = "android"
package_name = "com.company.myapp"
track = "internal" # internal | alpha | beta | production
keystore_path = "~/.shipper/keys/release.keystore"
keystore_alias = "release"
keystore_password_path = "~/.shipper/keys/keystore-password"
build_type = "bundle" # bundle (AAB) | apk
[versioning]
strategy = "semver"
auto_increment = true- Go to App Store Connect → Users and Access → Integrations → App Store Connect API
- Generate a key with Developer role
- Download
AuthKey_XXXXXX.p8— you can only download it once - Save to
~/.shipper/keys/AuthKey_XXXXXX.p8 - Note your Key ID and Issuer ID
chmod 600 ~/.shipper/keys/AuthKey_XXXXXX.p8- Go to Google Play Console → Setup → API access
- Link to a Google Cloud project
- Create a service account with Release Manager role
- Download the JSON key
- Save to
~/.shipper/keys/play-store-sa.json
chmod 600 ~/.shipper/keys/play-store-sa.json# Generate a new keystore (if you don't have one)
keytool -genkey -v \
-keystore ~/.shipper/keys/release.keystore \
-alias release \
-keyalg RSA -keysize 2048 \
-validity 10000
# Save the password to a file
echo "your-keystore-password" > ~/.shipper/keys/keystore-password
chmod 600 ~/.shipper/keys/keystore-password
chmod 600 ~/.shipper/keys/release.keystoreshipper deploy ios
│
├─ 0. Auto-install signing check Keychain + profiles, install from ~/.shipper/keys/<bundle_id>/
├─ 1. Bump build number app.json or Info.plist
├─ 2. expo prebuild (Expo / React Native projects only)
├─ 3. pod install (if Podfile exists)
├─ 4. xcodebuild archive → build/shipper/*.xcarchive
├─ 5. xcodebuild -export → build/shipper/ipa/*.ipa
├─ 6. xcrun altool upload → App Store Connect / TestFlight
├─ 7. Poll processing state → wait for VALID
└─ 8. Notify → Telegram / Slack
Prerequisites: macOS, Xcode, CocoaPods (for Expo/React Native projects)
shipper deploy android
│
├─ 1. Bump versionCode app.json or build.gradle
├─ 2. expo prebuild (Expo / React Native projects only)
├─ 3. ./gradlew bundleRelease → app-release.aab
├─ 4. Sign strip existing sigs + jarsigner (AAB) / apksigner (APK)
├─ 5. Play Store API v3 → upload + assign track + commit
└─ 6. Notify
Prerequisites: Android SDK, JDK (jarsigner), Java
Keystore: If no keystore exists at the configured path, Shipper auto-generates one with
keytool. Back it up — losing it means you can never update the app on Play Store.
Fastlane is the established standard, but it comes with real costs:
- Requires Ruby, Bundler, and a
Gemfilein every project pod installandbundle installadd minutes to every setup- Lanes are powerful but verbose — a basic TestFlight deploy needs 20+ lines
- Breaks frequently on macOS updates due to Ruby/gem compatibility
Shipper does the same thing with zero runtime dependencies. One binary, one config file.
EAS Submit is the official Expo solution, but:
- Requires an Expo account and paid plan for concurrent builds
- Builds run on Expo's cloud infrastructure — you can't inspect the environment
eas submitonly submits a pre-built binary; you still need to build separately
Shipper builds and submits in one step, entirely on your local machine.
Error:
[!] Invalid `RNReanimated.podspec` file: [Reanimated] Reanimated requires
the New Architecture to be enabled. If you have `RCT_NEW_ARCH_ENABLED=0`
set in your environment you should remove it.
Cause: react-native-reanimated v3+ requires New Architecture. If your
app.json has "newArchEnabled": false, Expo prebuild generates a Podfile
that sets ENV['RCT_NEW_ARCH_ENABLED'] = '0', which triggers this error.
Fix: Set "newArchEnabled": true in app.json:
{
"expo": {
"newArchEnabled": true
}
}This also affects users who upgrade to Xcode 16+ / macOS Sequoia or later since a fresh prebuild regenerates the Podfile and the flag is re-evaluated.
Error:
error: call to consteval function 'fmt::basic_format_string<...>::basic_format_string
<FMT_COMPILE_STRING, 0>' is not a constant expression
** ARCHIVE FAILED **
Cause: The fmt pod uses C++20 consteval, but Xcode defaults pods to
C++17. This surfaced on Xcode 16+ (Clang 16+) which became stricter about
consteval in non-C++20 translation units.
Fix: Add a config plugin to your Expo project that patches the generated
Podfile after every expo prebuild:
plugins/withFmtCpp20.js:
const { withDangerousMod } = require('@expo/config-plugins');
const fs = require('fs');
const path = require('path');
module.exports = function withFmtCpp20(config) {
return withDangerousMod(config, ['ios', (config) => {
const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile');
let contents = fs.readFileSync(podfilePath, 'utf-8');
const marker = '# [shipper] fmt c++20 fix';
if (!contents.includes(marker)) {
const patch = `
${marker}
installer.pods_project.targets.each do |target|
if target.name == 'fmt'
target.build_configurations.each do |config|
config.build_settings['CLANG_CXX_LANGUAGE_STANDARD'] = 'c++20'
end
end
end\n`;
contents = contents.replace('react_native_post_install(', patch + ' react_native_post_install(');
fs.writeFileSync(podfilePath, contents);
}
return config;
}]);
};app.json:
{
"expo": {
"plugins": ["./plugins/withFmtCpp20"]
}
}Proprietary — All rights reserved.