Loading...
Loading...
Automate iOS/macOS App Store Connect workflows - TestFlight, builds, submissions, screenshots, and metadata with the asc CLI
npx skill4agent add aradotso/devtools-skills app-store-connect-cliSkill by ara.so — Devtools Skills collection.
# Homebrew (macOS/Linux)
brew install asc
# Install script
curl -fsSL https://asccli.sh/install | bash
# Verify installation
asc version
asc --help.p8asc auth login \
--name "MyApp" \
--key-id "$ASC_KEY_ID" \
--issuer-id "$ASC_ISSUER_ID" \
--private-key /path/to/AuthKey.p8 \
--networkasc auth login \
--bypass-keychain \
--name "MyCIKey" \
--key-id "$ASC_KEY_ID" \
--issuer-id "$ASC_ISSUER_ID" \
--private-key /path/to/AuthKey.p8asc auth login \
--local \
--bypass-keychain \
--name "LocalKey" \
--key-id "$ASC_KEY_ID" \
--issuer-id "$ASC_ISSUER_ID" \
--private-key /path/to/AuthKey.p8# Check auth status
asc auth status --validate
# Run diagnostics
asc auth doctor
# List active profile
asc auth list# List all apps
asc apps list --output table
asc apps list --output json --pretty
# Get specific app info
asc apps info view --app "1234567890" --output json
# Search apps
asc apps list --output json | jq '.data[] | select(.attributes.name | contains("MyApp"))'# Upload IPA to App Store Connect
asc builds upload --app "1234567890" --ipa /path/to/MyApp.ipa
# List builds
asc builds list --app "1234567890" --output table
asc builds list --app "1234567890" --limit 10 --sort -uploadedDate
# Get next build number
asc builds next-build-number --app "1234567890" --version "1.2.3"
# Get build details
asc builds get --id "BUILD_ID" --output json# Publish to TestFlight
asc publish testflight \
--app "1234567890" \
--ipa /path/to/MyApp.ipa \
--group "Internal Testers" \
--wait
# List TestFlight groups
asc testflight groups list --app "1234567890" --output table
# Add testers to group
asc testflight testers add \
--group "GROUP_ID" \
--email user@example.com
# List feedback
asc testflight feedback list \
--app "1234567890" \
--paginate \
--output json
# List crashes
asc testflight crashes list \
--app "1234567890" \
--sort -createdDate \
--limit 10
# Get crash logs
asc testflight crashes log --submission-id "SUBMISSION_ID"# Full publish workflow (upload + attach + submit)
asc publish appstore \
--app "1234567890" \
--ipa /path/to/MyApp.ipa \
--version "1.2.3" \
--submit \
--confirm
# Stage release (without submitting)
asc release stage \
--app "1234567890" \
--version "1.2.3" \
--build "BUILD_ID" \
--copy-metadata-from "1.2.2" \
--dry-run
# Validate readiness
asc validate --app "1234567890" --version "1.2.3"
# Check submission status
asc submit status --version-id "VERSION_ID"
# Monitor status with auto-refresh
asc status --app "1234567890" --watch
# Cancel submission
asc submit cancel --version-id "VERSION_ID" --confirm# List versions
asc versions list --app "1234567890" --output table
# Create new version
asc versions create \
--app "1234567890" \
--version "1.3.0" \
--platform "IOS"
# Get version details
asc versions get --id "VERSION_ID" --output json# List localizations
asc localizations list \
--app "1234567890" \
--type app-info \
--output table
# Initialize metadata directory structure
asc metadata init \
--dir ./metadata \
--version "1.2.3" \
--locale "en-US"
# Apply metadata from directory
asc metadata apply \
--app "1234567890" \
--version "1.2.3" \
--dir ./metadata \
--dry-run
# Audit keywords (ASO checks)
asc metadata keywords audit \
--app "1234567890" \
--version "1.2.3" \
--blocked-terms-file ./blocked-terms.txt
# Sync metadata between versions
asc metadata sync \
--app "1234567890" \
--from-version "1.2.2" \
--to-version "1.2.3" \
--locale "en-US"# Plan screenshot layout
asc screenshots plan \
--app "1234567890" \
--version "1.2.3" \
--review-output-dir ./screenshots/review
# Apply screenshots
asc screenshots apply \
--app "1234567890" \
--version "1.2.3" \
--review-output-dir ./screenshots/review \
--confirm
# Upload screenshots for specific locale
# First, get version localization ID
asc localizations list \
--version "VERSION_ID" \
--output json \
--locale "en-US" | jq '.data[0].id'
# Then upload
asc screenshots upload \
--version-localization "VERSION_LOCALIZATION_ID" \
--path ./screenshots/en-US \
--device-type "IPHONE_65" \
--replace
# List existing screenshots
asc screenshots list \
--version-localization "VERSION_LOCALIZATION_ID" \
--output table# Check review status
asc review status --app "1234567890"
# Run review diagnostics
asc review doctor --app "1234567890"# List certificates
asc certificates list --output table
# List provisioning profiles
asc profiles list --output table
# List bundle IDs
asc bundle-ids list --output table
# Register new bundle ID
asc bundle-ids create \
--identifier "com.example.myapp" \
--name "My App" \
--platform "IOS"# Archive project
asc xcode archive \
--project MyApp.xcodeproj \
--scheme "MyApp" \
--archive-path ./build/MyApp.xcarchive
# Export IPA
asc xcode export \
--archive-path ./build/MyApp.xcarchive \
--export-options-plist ./ExportOptions.plist \
--export-path ./build/output# List workflows
asc xcode-cloud workflows list --output table
# Trigger workflow from PR
asc xcode-cloud run \
--workflow-id "WORKFLOW_ID" \
--pull-request-id "123"
# Rerun existing build
asc xcode-cloud run \
--source-run-id "BUILD_RUN_ID" \
--clean
# Get build run details
asc xcode-cloud build-runs get --id "BUILD_RUN_ID" --output json.asc/workflow.json{
"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",
"scheme": "MyApp",
"archive-path": "./build/MyApp.xcarchive"
}
},
{
"name": "export",
"command": "xcode export",
"args": {
"archive-path": "./build/MyApp.xcarchive",
"export-options-plist": "./ExportOptions.plist",
"export-path": "./build/output"
}
},
{
"name": "publish",
"command": "publish testflight",
"args": {
"app": "1234567890",
"ipa": "./build/output/MyApp.ipa",
"group": "Internal Testers",
"wait": true
}
}
]
},
"appstore_release": {
"description": "Submit to App Store",
"steps": [
{
"name": "validate",
"command": "validate",
"args": {
"app": "1234567890",
"version": "{{VERSION}}"
}
},
{
"name": "publish",
"command": "publish appstore",
"args": {
"app": "1234567890",
"ipa": "./build/output/MyApp.ipa",
"version": "{{VERSION}}",
"submit": true,
"confirm": true
}
}
]
}
}
}# Validate workflow
asc workflow validate
# Dry run
asc workflow run --dry-run testflight_beta VERSION:1.2.3
# Execute workflow
asc workflow run testflight_beta VERSION:1.2.3
# Execute App Store release
asc workflow run appstore_release VERSION:1.2.3name: 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"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:
- mainversion: 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"tablejson# Table output
asc apps list --output table
# JSON output
asc apps list --output json
# Pretty JSON (human-readable)
asc apps list --output json --pretty
# Markdown output
asc apps list --output markdown
# Set default via environment
export ASC_DEFAULT_OUTPUT=json# Extract app IDs
asc apps list --output json | jq -r '.data[].id'
# Filter by name
asc apps list --output json | jq '.data[] | select(.attributes.name == "MyApp")'
# Get build count
asc builds list --app "1234567890" --output json | jq '.data | length'
# Extract version strings
asc versions list --app "1234567890" --output json | jq -r '.data[].attributes.versionString'#!/bin/bash
set -e
APP_ID="1234567890"
VERSION="1.2.3"
IPA_PATH="./build/output/MyApp.ipa"
# 1. Get next build number
BUILD_NUMBER=$(asc builds next-build-number --app "$APP_ID" --version "$VERSION" --output json | jq -r '.buildNumber')
echo "Next build number: $BUILD_NUMBER"
# 2. Build (external Xcode process)
# xcodebuild archive ...
# xcodebuild -exportArchive ...
# 3. Upload and publish to TestFlight
asc publish testflight \
--app "$APP_ID" \
--ipa "$IPA_PATH" \
--group "Internal Testers" \
--wait
# 4. Wait for processing
sleep 60
# 5. Validate for App Store
asc validate --app "$APP_ID" --version "$VERSION"
# 6. Submit to App Store
asc publish appstore \
--app "$APP_ID" \
--version "$VERSION" \
--build "BUILD_ID" \
--submit \
--confirm
# 7. Monitor status
asc status --app "$APP_ID" --watch#!/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#!/bin/bash
APP_ID="1234567890"
VERSION_ID="VERSION_ID"
SCREENSHOTS_DIR="./screenshots"
# Get version localizations
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..."
# Upload for each device type
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"#!/bin/bash
APP_ID="1234567890"
OUTPUT_DIR="./crash_logs"
mkdir -p "$OUTPUT_DIR"
# Get recent crashes
CRASHES=$(asc testflight crashes list \
--app "$APP_ID" \
--sort -createdDate \
--limit 20 \
--output json)
# Download logs for each crash
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#!/bin/bash
APP_ID="1234567890"
REPORT_FILE="testflight_feedback_$(date +%Y%m%d).json"
# Collect all feedback
asc testflight feedback list \
--app "$APP_ID" \
--paginate \
--output json > "$REPORT_FILE"
# Generate summary
echo "Feedback Summary:"
jq '.data | group_by(.attributes.rating) | map({rating: .[0].attributes.rating, count: length})' "$REPORT_FILE"# Authentication
export ASC_KEY_ID="ABC123"
export ASC_ISSUER_ID="DEF456"
export ASC_PRIVATE_KEY_PATH="/path/to/AuthKey.p8"
# Bypass keychain (CI/CD)
export ASC_BYPASS_KEYCHAIN="1"
# Default output format
export ASC_DEFAULT_OUTPUT="json"
# API debugging
export ASC_DEBUG="api"
# Config file location
export ASC_CONFIG_PATH="/custom/path/config.json"# Check keychain access
asc auth doctor
# Retry with bypass (if keychain blocked)
ASC_BYPASS_KEYCHAIN=1 asc auth status --validate
# Validate active profile
asc auth status --validate
# Re-login with bypass
asc auth login \
--bypass-keychain \
--name "Retry" \
--key-id "$ASC_KEY_ID" \
--issuer-id "$ASC_ISSUER_ID" \
--private-key /path/to/AuthKey.p8# Enable API debugging
ASC_DEBUG=api asc apps list
# Or use flag
asc --api-debug apps list
# Capture detailed logs
asc --api-debug apps list 2>&1 | tee debug.log# Verify IPA integrity
unzip -t MyApp.ipa
# Check app ID matches
asc apps list --output json | jq '.data[] | select(.id == "1234567890")'
# Retry with explicit app ID
asc builds upload --app "1234567890" --ipa ./MyApp.ipa --verbose# List existing versions
asc versions list --app "1234567890" --output table
# Check version state
asc versions get --id "VERSION_ID" --output json | jq '.data.attributes.appStoreState'
# Delete draft version (if needed)
asc versions delete --id "VERSION_ID" --confirm# Verify device type compatibility
asc screenshots plan --app "1234567890" --version "1.2.3" --review-output-dir ./review
# Check localization ID
asc localizations list --version "VERSION_ID" --output json
# Use correct device type codes:
# IPHONE_65, IPHONE_67, IPHONE_61, IPAD_PRO_129, IPAD_PRO_3GEN_129asc validate --app "APP_ID" --version "VERSION"--dry-runasc metadata apply --dry-run --app "APP_ID" --version "VERSION" --dir ./metadata--watchasc status --app "APP_ID" --watch.p8--bypass-keychainasc apps list --output json | jq -r '.data[].id'.asc/workflow.json--dry-run--waitasc publish testflight --wait --app "APP_ID" --ipa ./app.ipa