| name | app-store-connect-cli |
| description | Automate iOS/macOS App Store Connect workflows - TestFlight, builds, submissions, screenshots, and metadata with the asc CLI |
| triggers | ["upload build to testflight","submit app to app store","manage app store screenshots","check app review status","list testflight builds","sync app metadata to app store","automate app store connect workflow","get app store analytics"] |
App Store Connect CLI (asc)
Skill by ara.so — Devtools Skills collection.
Fast, scriptable CLI for the App Store Connect API. Automate TestFlight, builds, submissions, signing, analytics, screenshots, subscriptions, and more. JSON-first, no interactive prompts - perfect for CI/CD and automation.
Installation
brew install asc
curl -fsSL https://asccli.sh/install | bash
asc version
asc --help
For Windows, download signed binaries from the releases page.
Authentication
Generate API Keys
- Visit https://appstoreconnect.apple.com/access/integrations/api
- Create a new API key with appropriate role (Admin for full access)
- Download the
.p8 private key file
- Note the Key ID and Issuer ID
Configure Authentication
Interactive environments (with keychain):
asc auth login \
--name "MyApp" \
--key-id "$ASC_KEY_ID" \
--issuer-id "$ASC_ISSUER_ID" \
--private-key /path/to/AuthKey.p8 \
--network
CI/CD or headless environments:
asc auth login \
--bypass-keychain \
--name "MyCIKey" \
--key-id "$ASC_KEY_ID" \
--issuer-id "$ASC_ISSUER_ID" \
--private-key /path/to/AuthKey.p8
Repo-local credentials:
asc auth login \
--local \
--bypass-keychain \
--name "LocalKey" \
--key-id "$ASC_KEY_ID" \
--issuer-id "$ASC_ISSUER_ID" \
--private-key /path/to/AuthKey.p8
Validate Authentication
asc auth status --validate
asc auth doctor
asc auth list
Core Commands
Apps Management
asc apps list --output table
asc apps list --output json --pretty
asc apps info view --app "1234567890" --output json
asc apps list --output json | jq '.data[] | select(.attributes.name | contains("MyApp"))'
Builds
asc builds upload --app "1234567890" --ipa /path/to/MyApp.ipa
asc builds list --app "1234567890" --output table
asc builds list --app "1234567890" --limit 10 --sort -uploadedDate
asc builds next-build-number --app "1234567890" --version "1.2.3"
asc builds get --id "BUILD_ID" --output json
TestFlight
asc publish testflight \
--app "1234567890" \
--ipa /path/to/MyApp.ipa \
--group "Internal Testers" \
--wait
asc testflight groups list --app "1234567890" --output table
asc testflight testers add \
--group "GROUP_ID" \
--email user@example.com
asc testflight feedback list \
--app "1234567890" \
--paginate \
--output json
asc testflight crashes list \
--app "1234567890" \
--sort -createdDate \
--limit 10
asc testflight crashes log --submission-id "SUBMISSION_ID"
App Store Submissions
asc publish appstore \
--app "1234567890" \
--ipa /path/to/MyApp.ipa \
--version "1.2.3" \
--submit \
--confirm
asc release stage \
--app "1234567890" \
--version "1.2.3" \
--build "BUILD_ID" \
--copy-metadata-from "1.2.2" \
--dry-run
asc validate --app "1234567890" --version "1.2.3"
asc submit status --version-id "VERSION_ID"
asc status --app "1234567890" --watch
asc submit cancel --version-id "VERSION_ID" --confirm
Versions
asc versions list --app "1234567890" --output table
asc versions create \
--app "1234567890" \
--version "1.3.0" \
--platform "IOS"
asc versions get --id "VERSION_ID" --output json
Metadata & Localization
asc localizations list \
--app "1234567890" \
--type app-info \
--output table
asc metadata init \
--dir ./metadata \
--version "1.2.3" \
--locale "en-US"
asc metadata apply \
--app "1234567890" \
--version "1.2.3" \
--dir ./metadata \
--dry-run
asc metadata keywords audit \
--app "1234567890" \
--version "1.2.3" \
--blocked-terms-file ./blocked-terms.txt
asc metadata sync \
--app "1234567890" \
--from-version "1.2.2" \
--to-version "1.2.3" \
--locale "en-US"
Screenshots
asc screenshots plan \
--app "1234567890" \
--version "1.2.3" \
--review-output-dir ./screenshots/review
asc screenshots apply \
--app "1234567890" \
--version "1.2.3" \
--review-output-dir ./screenshots/review \
--confirm
asc localizations list \
--version "VERSION_ID" \
--output json \
--locale "en-US" | jq '.data[0].id'
asc screenshots upload \
--version-localization "VERSION_LOCALIZATION_ID" \
--path ./screenshots/en-US \
--device-type "IPHONE_65" \
--replace
asc screenshots list \
--version-localization "VERSION_LOCALIZATION_ID" \
--output table
Review Status
asc review status --app "1234567890"
asc review doctor --app "1234567890"
Signing & Certificates
asc certificates list --output table
asc profiles list --output table
asc bundle-ids list --output table
asc bundle-ids create \
--identifier "com.example.myapp" \
--name "My App" \
--platform "IOS"
Xcode Integration
asc xcode archive \
--project MyApp.xcodeproj \
--scheme "MyApp" \
--archive-path ./build/MyApp.xcarchive
asc xcode export \
--archive-path ./build/MyApp.xcarchive \
--export-options-plist ./ExportOptions.plist \
--export-path ./build/output
Xcode Cloud
asc xcode-cloud workflows list --output table
asc xcode-cloud run \
--workflow-id "WORKFLOW_ID" \
--pull-request-id "123"
asc xcode-cloud run \
--source-run-id "BUILD_RUN_ID" \
--clean
asc xcode-cloud build-runs get --id "BUILD_RUN_ID" --output json
Workflows
Workflow Configuration
Create .asc/workflow.json for reusable workflows:
{
"workflows": {
"testflight_beta": {
"description": "Build and deploy to TestFlight",
"steps": [
{
"name": "get_next_build",
"command": "builds next-build-number",
"args": {
"app": "1234567890",
"version": "{{VERSION}}"
},
"capture": "BUILD_NUMBER"
},
{
"name": "archive",
"command": "xcode archive",
"args": {
"project": "MyApp.xcodeproj"
Run Workflows
asc workflow validate
asc workflow run --dry-run testflight_beta VERSION:1.2.3
asc workflow run testflight_beta VERSION:1.2.3
asc workflow run appstore_release VERSION:1.2.3
CI/CD Integration
GitHub Actions
name: TestFlight Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Install asc
run: brew install asc
- name: Configure Auth
run: |
echo "${{ secrets.ASC_PRIVATE_KEY }}" > AuthKey.p8
asc auth login \
--bypass-keychain \
--name "CI" \
--key-id "${{ secrets.ASC_KEY_ID }}" \
--issuer-id "${{ secrets.ASC_ISSUER_ID }}" \
--private-key ./AuthKey.p8
- name: Build and Deploy
run: |
asc workflow run testflight_beta VERSION:1.2.3
env:
ASC_BYPASS_KEYCHAIN: "1"
GitLab CI
deploy_testflight:
stage: deploy
tags:
- macos
script:
- brew install asc
- echo "$ASC_PRIVATE_KEY" > AuthKey.p8
- |
asc auth login \
--bypass-keychain \
--name "GitLab CI" \
--key-id "$ASC_KEY_ID" \
--issuer-id "$ASC_ISSUER_ID" \
--private-key ./AuthKey.p8
- asc publish testflight --app "$APP_ID" --ipa ./MyApp.ipa --group "Internal" --wait
variables:
ASC_BYPASS_KEYCHAIN: "1"
only:
- main
CircleCI
version: 2.1
jobs:
deploy:
macos:
xcode: 15.0.0
steps:
- checkout
- run:
name: Install asc
command: brew install asc
- run:
name: Configure Auth
command: |
echo "$ASC_PRIVATE_KEY" > AuthKey.p8
asc auth login \
--bypass-keychain \
--name "CircleCI" \
--key-id "$ASC_KEY_ID" \
--issuer-id "$ASC_ISSUER_ID" \
--private-key ./AuthKey.p8
- run:
name: Deploy
command: asc workflow run testflight_beta VERSION:${CIRCLE_TAG}
environment:
ASC_BYPASS_KEYCHAIN: "1"
Output Formats
The CLI auto-detects output format based on context:
- TTY (interactive):
table format
- Non-TTY (pipes/CI):
json format
Explicit Format Control
asc apps list --output table
asc apps list --output json
asc apps list --output json --pretty
asc apps list --output markdown
export ASC_DEFAULT_OUTPUT=json
Parsing JSON Output
asc apps list --output json | jq -r '.data[].id'
asc apps list --output json | jq '.data[] | select(.attributes.name == "MyApp")'
asc builds list --app "1234567890" --output json | jq '.data | length'
asc versions list --app "1234567890" --output json | jq -r '.data[].attributes.versionString'
Common Patterns
Complete Release Workflow
#!/bin/bash
set -e
APP_ID="1234567890"
VERSION="1.2.3"
IPA_PATH="./build/output/MyApp.ipa"
BUILD_NUMBER=$(asc builds next-build-number --app "$APP_ID" --version "$VERSION" --output json | jq -r '.buildNumber')
echo "Next build number: $BUILD_NUMBER"
asc publish testflight \
--app "$APP_ID" \
--ipa "$IPA_PATH" \
--group "Internal Testers" \
--wait
sleep 60
asc validate --app "$APP_ID" --version "$VERSION"
asc publish appstore \
--app "$APP_ID" \
--version "$VERSION" \
--build "BUILD_ID" \
--submit \
--confirm
asc status --app "$APP_ID" --watch
Metadata Sync Between Versions
#!/bin/bash
APP_ID="1234567890"
FROM_VERSION="1.2.2"
TO_VERSION="1.2.3"
LOCALES=("en-US" "es-ES" "fr-FR" "de-DE")
for locale in "${LOCALES[@]}"; do
echo "Syncing $locale..."
asc metadata sync \
--app "$APP_ID" \
--from-version "$FROM_VERSION" \
--to-version "$TO_VERSION" \
--locale "$locale"
done
Screenshot Upload Pipeline
#!/bin/bash
APP_ID="1234567890"
VERSION_ID="VERSION_ID"
SCREENSHOTS_DIR="./screenshots"
LOCALIZATIONS=$(asc localizations list \
--version "$VERSION_ID" \
--output json | jq -r '.data[] | "\(.attributes.locale)|\(.id)"')
while IFS='|' read -r locale loc_id; do
echo "Uploading screenshots for $locale..."
for device in "IPHONE_65" "IPHONE_67" "IPAD_PRO_129"; do
if [ -d "$SCREENSHOTS_DIR/$locale/$device" ]; then
asc screenshots upload \
--version-localization "$loc_id" \
--path "$SCREENSHOTS_DIR/$locale/$device" \
--device-type "$device" \
--replace
fi
done
done <<< "$LOCALIZATIONS"
Crash Log Collection
#!/bin/bash
APP_ID="1234567890"
OUTPUT_DIR="./crash_logs"
mkdir -p "$OUTPUT_DIR"
CRASHES=$(asc testflight crashes list \
--app "$APP_ID" \
--sort -createdDate \
--limit 20 \
--output json)
echo "$CRASHES" | jq -r '.data[].id' | while read -r submission_id; do
echo "Downloading crash log: $submission_id"
asc testflight crashes log \
--submission-id "$submission_id" > "$OUTPUT_DIR/$submission_id.log"
done
Automated TestFlight Feedback Reports
#!/bin/bash
APP_ID="1234567890"
REPORT_FILE="testflight_feedback_$(date +%Y%m%d).json"
asc testflight feedback list \
--app "$APP_ID" \
--paginate \
--output json > "$REPORT_FILE"
echo "Feedback Summary:"
jq '.data | group_by(.attributes.rating) | map({rating: .[0].attributes.rating, count: length})' "$REPORT_FILE"
Environment Variables
export ASC_KEY_ID="ABC123"
export ASC_ISSUER_ID="DEF456"
export ASC_PRIVATE_KEY_PATH="/path/to/AuthKey.p8"
export ASC_BYPASS_KEYCHAIN="1"
export ASC_DEFAULT_OUTPUT="json"
export ASC_DEBUG="api"
export ASC_CONFIG_PATH="/custom/path/config.json"
Troubleshooting
Authentication Issues
asc auth doctor
ASC_BYPASS_KEYCHAIN=1 asc auth status --validate
asc auth status --validate
asc auth login \
--bypass-keychain \
--name "Retry" \
--key-id "$ASC_KEY_ID" \
--issuer-id "$ASC_ISSUER_ID" \
--private-key /path/to/AuthKey.p8
API Debugging
ASC_DEBUG=api asc apps list
asc --api-debug apps list
asc --api-debug apps list 2>&1 | tee debug.log
Build Upload Failures
unzip -t MyApp.ipa
asc apps list --output json | jq '.data[] | select(.id == "1234567890")'
asc builds upload --app "1234567890" --ipa ./MyApp.ipa --verbose
Version Conflicts
asc versions list --app "1234567890" --output table
asc versions get --id "VERSION_ID" --output json | jq '.data.attributes.appStoreState'
asc versions delete --id "VERSION_ID" --confirm
Screenshot Upload Issues
asc screenshots plan --app "1234567890" --version "1.2.3" --review-output-dir ./review
asc localizations list --version "VERSION_ID" --output json
Best Practices
-
Always validate before submission:
asc validate --app "APP_ID" --version "VERSION"
-
Use --dry-run for destructive operations:
asc metadata apply --dry-run --app "APP_ID" --version "VERSION" --dir ./metadata
-
Monitor submissions with --watch:
asc status --app "APP_ID" --watch
-
Store credentials securely in CI:
- Use GitHub Secrets, GitLab Variables, etc.
- Never commit
.p8 files
- Use
--bypass-keychain in CI
-
Parse JSON programmatically:
asc apps list --output json | jq -r '.data[].id'
-
Use workflows for complex pipelines:
- Define in
.asc/workflow.json
- Version control your workflows
- Test with
--dry-run
-
Leverage --wait for synchronous operations:
asc publish testflight --wait --app "APP_ID" --ipa ./app.ipa
Additional Resources