appuploader-cli Tutorial: Uploading an IPA to the App Store from the Command Line on Windows
One command pushes your built IPA to App Store Connect — no Mac, no Xcode:
1appuploader-cli upload -f Payload.ipa -u dev@example.com -p abcd-efgh-ijkl-mnop
appuploader-cli is the command-line tool bundled with AppUploader (开心上架); once the app is installed, it’s right there in the install directory. Below is a walkthrough from scratch: how to find it, what you need before your first upload, which login method to use for teams and pipelines, and how to diagnose failed uploads.
When you need the command line
If a couple of clicks in the GUI can submit your build, most people don’t need the CLI. It’s really for these three situations:
- The build is produced automatically. CI packages a build every time you commit, and then someone still has to click upload manually — that step should be wired in.
- The upload has to be run by someone else. Outsourcing, agency work, releasing multiple apps in rotation — a script beats verbally explaining which button to click.
- On a machine with no GUI. Such as a company build server with only a remote terminal.
If you only publish manually once in a while, clicking twice in the GUI is actually faster; the value of the CLI is in repeated execution.
Before you start
1. You need a signed package first. appuploader-cli handles the “hand the package to Apple” step; it does not compile or sign. You must already have a built .ipa (iOS) or .pkg (macOS).
2. Find the command. Once AppUploader is installed, it’s already in the install directory:
| System | Location |
|---|---|
| Windows | appuploader-cli.exe in the same directory as the main program AppUploader.exe |
| macOS | AppUploader.app/Contents/Resources/appuploader-cli |
Open a terminal in that directory and you can use it directly. If you don’t want to cd there every time, add the directory to your system PATH (on Windows: Settings → System → About → Advanced system settings → Environment Variables → select Path → Edit → New, paste the directory path, then reopen a terminal for it to take effect).
Run one command to confirm it works:
1appuploader-cli --help
If it lists upload and info, you’ve found the right one.
First upload: using an app-specific password
The most convenient credential for logging in to Apple from the command line is an app-specific password — Apple created these specifically for third-party tools, they do not trigger two-factor authentication, and you don’t have to confirm on your phone for every upload.
Generate one first: sign in at https://account.apple.com/account/manage, find “App-Specific Passwords,” and create a new one. You’ll get a password shaped like abcd-efgh-ijkl-mnop, shown only once, so copy and save it right away.
Then:
1appuploader-cli upload -f Payload.ipa -u dev@example.com -p abcd-efgh-ijkl-mnop
Three arguments: -f is the path to the package (you can also omit -f and put the path at the end of the command), -u is the Apple ID email, and -p is the app-specific password you just created.
Make sure
-pis the app-specific password, not the password you normally use to sign in to your Apple ID. An app-specific password is always 16 letters with three hyphens in the middle, looks nothing like your login password, and you can tell at a glance whether you got it wrong.
After you press Enter, progress prints line by line, and a few minutes later you’ll see this line, meaning success:
> upload finished successfully
On failure it’s > upload finished with error:, followed directly by the reason Apple gave — a duplicate version number, a signature mismatch, a missing Info.plist field, and so on; just fix accordingly.
In scripts you don’t need to read the logs: on failure the command’s exit code is non-zero, so checking the exit code is enough.
Don’t rush to App Store Connect to look for the build right after uploading — Apple takes anywhere from a few minutes to a few tens of minutes to process the package, and only after that does it appear in the “Builds” list, at which point you can select it for review.
Uploading macOS / tvOS / visionOS packages
It’s treated as iOS by default; add --type for other platforms:
1appuploader-cli upload -u dev@example.com -p abcd-efgh-ijkl-mnop --type osx App.pkg
There are only four possible values: ios (default), osx, appletvos, xros (visionOS).
Teams and CI: switch to API key login
The approach in the previous section has a problem: the command carries an individual’s Apple ID and password. If that person leaves, changes their password, or simply doesn’t want to hand their account to the pipeline, the whole automation breaks.
For team scenarios, an App Store Connect API key is more appropriate: it belongs to the team rather than an individual, can be revoked independently, and doesn’t involve two-factor authentication.
Generate one at https://appstoreconnect.apple.com/access/api (requires Account Holder or Admin permission) and you’ll get three things:
- Key ID (a string of uppercase letters and digits)
- Issuer ID (at the top of the page, shaped like
69a6de78-4459-47e3-e053-5b8c7c11a4d1) - The
.p8private key file — downloadable only once, Apple gives no second chance, so save it immediately
Then replace -u -p with these three arguments:
1appuploader-cli upload -f Payload.ipa \
2 --api-key UK29KBAX9X \
3 --api-issuer 69a6de78-4459-47e3-e053-5b8c7c11a4d1 \
4 --private-key AuthKey_UK29KBAX9X.p8
The two login methods are mutually exclusive; mixing them is rejected outright.
Confirm two things before using an API key:
- You can only upload
.ipa. This method relies on the Bundle ID in the package to find the corresponding app on App Store Connect, which can’t be obtained from a.pkg. For macOS packages, stick with the app-specific password from the previous section. - An app must already exist in App Store Connect for this Bundle ID. If not, you’ll get
no App Store Connect app found for bundle ID— create the app first, then upload.
Wiring it into a pipeline
The idea is one sentence: after the build produces the IPA, add an upload step, and pass credentials via the CI’s built-in secrets rather than writing them in plaintext in the script.
GitHub Actions (store the three items as repository secrets):
1- name: Upload to App Store Connect
2 env:
3 ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
4 ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
5 ASC_P8: ${{ secrets.ASC_P8 }}
6 run: |
7 printf '%s' "$ASC_P8" > AuthKey.p8
8 appuploader-cli upload -f build/App.ipa \
9 --api-key "$ASC_KEY_ID" \
10 --api-issuer "$ASC_ISSUER_ID" \
11 --private-key AuthKey.p8
12 rm -f AuthKey.p8
Don’t skip that last rm line — the private key shouldn’t be left in the Runner’s working directory.
Jenkins / generic shell:
1set -e # abort the whole pipeline if the upload fails
2appuploader-cli upload -f "$WORKSPACE/build/App.ipa" \
3 -u "$APPLE_ID" -p "$APP_SPECIFIC_PASSWORD"
Windows batch / PowerShell:
1& "C:\Program Files\AppUploader\appuploader-cli.exe" upload `
2 -f .\build\App.ipa -u $env:APPLE_ID -p $env:APP_PASSWORD
3if ($LASTEXITCODE -ne 0) { throw "Upload failed" }
The whole process needs no Mac; even if the build machine is Windows, this step still runs fine.
By the way: inspecting a package without uploading
There’s also the info command, which doesn’t upload over the network — it only reads the package locally and generates the resource description file App Store needs, AppStoreInfo.plist:
1appuploader-cli info -u dev@example.com Payload.ipa -o AppStoreInfo.plist
Two uses: confirm the package itself is fine before uploading (on success it prints the package’s Bundle ID and size, so you can double-check you’re uploading the right one); or your process uses a different upload tool and you’re only missing this plist.
Without -o it prints to the terminal, which is handy for piping. info only supports the -u account form, with no API key mode.