Digital

mobile-stores-cicd: Automating App Store Connect with a 37-Command Python Toolkit

Managing a small portfolio of iOS apps through App Store Connect is deceptively tedious. Each release cycle means rotating certificates, regenerating provisioning profiles, updating metadata across four locales, composing device-framed screenshots, attaching TestFlight builds, submitting for review, and controlling phased rollouts. Do that for three apps and the manual overhead starts eating days. I built mobile-stores-cicd to collapse all of that into a single CLI toolkit that talks directly to the App Store Connect API, renders metadata from Jinja2 templates, automates screenshot capture on Simulator, and handles everything from cert rotation to phased release control.

This article walks through the full architecture: 37 CLI commands organized around certificate management, metadata templating, screenshot pipelines, release automation, a self-hosted IPA sideload system with Cloudflare R2 and Telegram delivery, Crowdin-driven translation sync, and an optional Streamlit dashboard. The toolkit is macOS-first (it needs Keychain and Xcode tooling), Python-based, and built around a safety-first model where every destructive operation has a --dry-run flag. The repository contains over 13,000 words of documentation across 21 files covering setup, workflows, troubleshooting, and security handling.

Why another CI/CD tool for mobile stores

Fastlane covers a lot of ground, but it is Ruby-based, opinionated about project structure, and carries years of accumulated complexity for use cases that don’t apply to a three-app portfolio. What I needed was narrower and deeper: a Python toolkit that wraps the App Store Connect REST API directly, handles the specific metadata model my apps use (Jinja2 templates with locale-specific YAML values), automates screenshot composition with device frames and localized headlines, and integrates with Cloudflare R2 for a self-hosted sideload pipeline.

The tool currently automates the full iOS lifecycle for three production apps (two RocketChat instances and a Flutter-based VaultApprover) across four locales (en-US, ru, ar-SA, zh-Hans). Google Play infrastructure is scaffolded — service account authentication is documented in android/SERVICE_ACCOUNT_SETUP.md, the credential path is standardized, and metadata/apps.yaml already supports Android-specific fields (package name, track, keystore path) — but the CLI commands are not yet wired. The immediate iOS coverage includes:

  • Certificate inventory with SHA-1 fingerprints and expiry tracking across Keychain, local profiles, and ASC server state
  • Provisioning profile regeneration after certificate rotation, with optional local save for Xcode pickup
  • Metadata pull/render/push with five-level template precedence and StrictUndefined fail-fast rendering
  • Screenshot capture on iOS Simulator via Maestro flows, composition with Pillow in two styles (marketing and overlay), validation against Apple dimension rules, and multipart S3 upload to ASC
  • Draft version creation, TestFlight build attachment, App Review submission with pre-flight checks, and phased rollout control
  • Customer review management, sales reporting, and TestFlight beta group operations
  • Self-hosted IPA sideloading with code re-signing, .deb tweak injection via cyan, OTA manifest publication, and Telegram delivery
  • Crowdin translation sync for App Store metadata, screenshot headlines, and in-app strings with locale remapping

Architecture and design principles

The toolkit is a single Python package with a modular CLI entry point (ios/cli.py) that loads 37 subcommands. Two shell wrappers provide access: ./apple <command> for direct use and ./launch for an interactive menu via questionary. The core client (ios/client.py) handles App Store Connect API authentication using ES256 JWT tokens generated from a .p8 key, with automatic pagination for list endpoints.

The repository layout separates concerns cleanly:

ios/                  ASC client, CLI, Keychain helpers, 37 commands
ios/sideload/         IPA re-sign, OTA manifest, R2 publish pipeline
ios/ui/               Optional Streamlit dashboard (8 view pages)
metadata/             App catalog, templates, per-app values, screenshots
.rendered/            Generated metadata payloads (gitignored)
.cache/               Local state: UI prefs, ASC baselines, sideload state
secrets/              Credentials (.p8, .p12, service account JSON)
tests/                15+ pytest files for non-network regression
docs/                 6 workflow guides + troubleshooting (1,600+ words)

The app catalog (metadata/apps.yaml) is the single source of truth: it maps each app slug to its ASC app ID, bundle ID, display name, template selection, target locales, optional categories, repo metadata, and an optional Android config block. Every command that operates on an app resolves it through this catalog. The Streamlit dashboard shows a green “Android” pill next to the blue “iOS” pill when the android block is present, even before Android commands are implemented.

The environment is configured through 16 variables in a .env file: three for ASC credentials (ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_PATH), three for Cloudflare R2 (R2_ENDPOINT, R2_ACCESS_KEY, R2_SECRET_KEY), two for Telegram (TG_BOT_TOKEN, TG_CHAT_ID), Crowdin and GitHub tokens, and several operational paths. The docs/SETUP.md guide walks through every variable with verification steps.

Certificate and provisioning profile management

Apple certificate rotation is one of those tasks that feels simple until you have three apps sharing profiles across development and distribution certificates. The toolkit provides a complete workflow:

./apple list-certs                          # SHA-1 fingerprints, types, expiry
./apple list-profiles --sha1 <OLD_SHA1>     # profiles referencing the old cert
./apple regenerate-profiles \
  --old-sha1 <OLD> --new-sha1 <NEW> \
  --type IOS_APP_STORE --save-local         # re-issue on ASC + save to Xcode
./apple cleanup --dry-run                   # preview stale profile removal
./apple cleanup                             # back up stale profiles, delete expired

The inventory command gives a read-only cross-reference of three layers: what Keychain has installed locally, what .mobileprovision files exist in the Xcode provisioning profile directory, and what ASC reports server-side. The cleanup command backs up stale local profiles before removing them and deletes expired Keychain identities, always with a --dry-run path first. The troubleshooting guide (docs/TROUBLESHOOTING.md) covers common failure modes like “Xcode Does Not See Reissued Profiles” and the recovery procedure for each.

Metadata workflow: templates, rendering, and push

The metadata system is where this toolkit diverges most from Fastlane. Instead of flat text files per locale, it uses Jinja2 templates with a five-level value precedence:

  1. Common defaultsmetadata/templates/_common/values/<locale>.yaml
  2. Template valuesmetadata/templates/<template>/values/<locale>.yaml
  3. App brandmetadata/<slug>/brand.yaml
  4. App releasemetadata/<slug>/release.yaml
  5. App locale overridesmetadata/<slug>/locales/<locale>.yaml

This means a shared boilerplate (privacy policy URL, support URL, copyright template) lives in one place while app-specific keywords and release notes are maintained per-app. Templates use StrictUndefined so a missing variable fails the render immediately rather than silently producing blank output. For cases where Jinja2 templates are too rigid, a raw override mechanism bypasses rendering entirely: files placed in metadata/<slug>/overrides/<scope>/<locale>/ are pushed to ASC as-is.

The pipeline is straightforward:

./apple metadata-pull --app el-hub --version latest    # pull current ASC state
# edit source YAML or Jinja2 templates
./apple metadata-render --app el-hub --show            # preview rendered output
./apple metadata-push --app el-hub --dry-run           # diff against ASC
./apple metadata-push --app el-hub                     # push to ASC

The push command has a built-in safety guard: it skips overwriting non-empty ASC values with empty rendered values. If a template accidentally produces blank output for a required field, the existing App Store content stays untouched. The metadata workflow covers two surfaces: app-info fields (name, subtitle, privacy URL, categories — shared across all versions) and version-localization fields (description, keywords, release notes, promotional text, screenshots — per-version).

Screenshot pipeline: from Simulator to App Store

Screenshot automation was the most time-consuming part to build and the most satisfying to use. The pipeline covers raw capture on iOS Simulator, localized composition with device frames and headlines, dimension validation, and upload to ASC via Apple’s multipart S3 flow. The result is a fully automated path from source code to published App Store screenshots across all locales and device classes.

Each app’s screenshot assets live under metadata/<slug>/screenshots/ with a specific directory structure:

screenshots/
├── _source/              raw UI captures (per-locale and per-device subdirs)
│   ├── 01_lock_ui.png    base English screenshots
│   ├── ru/               locale-specific captures
│   ├── ar-SA/
│   ├── _ipad/            device-specific captures
│   └── README.md         capture instructions
├── _capture.yaml         capture contract: build config, Simulator, locales, modes
├── _content/             per-locale headline YAML
│   ├── en-US.yaml        headline: "Locked until it's you"
│   ├── ru.yaml           headline: "Открывается только для вас"
│   └── ar-SA.yaml        headline: "تُفتح لك أنت فقط"
├── _frames/              transparent device frame PNGs
├── _maestro/             Maestro flow files for automated UI capture
├── screens.yaml          compositor config: screen catalog, colors, fonts, insets
└── en-US/APP_IPHONE_69/  final composed screenshots ready for upload

The capture harness (screenshots-capture) boots the configured Simulator, applies locale overrides via -AppleLanguages and -AppleLocale launch arguments, installs the app, runs Maestro flows to navigate to each screen, and saves raw PNGs. For quick iterations, screenshots-snap provides single-screen capture when you already have a running Simulator session. The _capture.yaml contract defines the project path, build command, Simulator configuration, supported locales, and capture modes for each app.

Here is what the pipeline produces. On the left, a raw UI capture straight from iOS Simulator. On the right, the composed output with a device frame and localized marketing headline, ready for App Store upload:

Raw UI capture from iOS Simulator — input to the screenshot composition pipeline
Raw UI capture from Simulator
Composed App Store screenshot — English locale with device frame and marketing headline
Composed: device frame + headline

The composition engine (screenshots-generate) combines raw UI screenshots with device frames and localized headlines in two styles: marketing (headline in a dedicated top band, phone mockup below on a dark gradient) and overlay (full-bleed UI with headline on a translucent band). The screens.yaml file defines the screen catalog, background colors, typography settings, frame insets, and corner radii for each composition. It handles Arabic and Chinese font fallbacks automatically so non-Latin headlines render correctly on macOS.

The same pipeline handles multiple locales and device classes. Here is the Arabic version of the same screen (note the RTL headline) and the iPad Pro composition:

Composed App Store screenshot — Arabic locale with RTL headline and device frame
Arabic locale — RTL headline
Composed App Store screenshot — iPad Pro with device frame and marketing headline
iPad Pro 12.9″ composition

The validation step (screenshots-validate) checks Apple dimension requirements for each device class (including the APP_IPHONE_69 size that ASC routes under APP_IPHONE_67 internally), verifies locale coverage, and flags unknown device folders or sets exceeding the 10-screenshot limit. The upload step (screenshots-push) handles ASC’s multipart S3 upload protocol, with --replace to delete existing sets before uploading new ones.

Release automation: from draft to phased rollout

The release workflow chains commands that cover the entire App Review submission path. The docs/RELEASE_WORKFLOW.md guide documents the recommended end-to-end sequence:

./apple apps-audit --app el-hub                       # inspect current state
./apple version-create --app el-hub --version 4.71.4  # create draft version
./apple metadata-push --app el-hub                    # push metadata
./apple screenshots-push --app el-hub --replace       # upload screenshots
./apple testflight-list --app el-hub                  # inspect available builds
./apple build-attach --app el-hub --build-version 23  # attach build
./apple version-submit --app el-hub --dry-run         # pre-flight checks
./apple version-submit --app el-hub                   # submit for review
./apple phased-release --app el-hub --action status   # monitor rollout

The version-submit command runs pre-flight checks before submission: it verifies that a build is attached, required version-localization fields are filled, whatsNew exists for non-1.0 versions, and App Review contact data is accessible. The --force flag bypasses these checks when you know what you’re doing.

Phased rollout control supports six actions: status, start, pause, resume, complete, and stop. Post-release operations include sales-report for fetching and aggregating ASC sales data and reviews-list / reviews-respond for customer review management. Combined with apps-audit for pre-submission state inspection, this covers the full release lifecycle without opening App Store Connect in a browser.

Sideload pipeline: re-signing and tweak injection

This is the feature that gets the most questions. The sideload subsystem lets you take a third-party IPA (from a GitHub release, a direct URL, or a .deb tweak injected into a decrypted base app), re-sign it with your own Apple Developer certificate, upload the signed IPA and OTA manifest to Cloudflare R2, and deliver the install link via Telegram with a QR code.

The bootstrap is a one-time sequence:

./apple sideload-init --email you@example.com     # issue new signing cert
# or: ./apple sideload-import-cert --p12 existing.p12  # reuse existing .p12
./apple sideload-register-device                  # register iPhone (USB auto-detect)
./apple sideload-tg-init                          # bind Telegram chat for delivery

After that, publishing a ready-made IPA is one command:

# From a GitHub release:
./apple sideload-install --repo https://github.com/SoCuul/SCInsta --refresh-altsource

# From a direct IPA URL:
./apple sideload-install --ipa-url https://example.com/app.ipa

What happens under the hood: the command downloads the latest IPA from the GitHub release, reads bundle metadata, creates or reuses the bundle ID in App Store Connect (rewriting it into the ae.ilia.sideload.* namespace when Apple rejects upstream IDs), creates or reuses the provisioning profile for the registered device, re-signs the IPA with zsign, uploads the signed IPA and generated .plist manifest to Cloudflare R2, optionally rebuilds an AltStore/SideStore-compatible source.json via --refresh-altsource, and sends the install URL and QR code to Telegram.

For tweak injection, most modern tweak repos (Instagram mods, YouTube mods) ship .deb packages rather than ready-to-sideload IPAs. The --build-deb flag handles this entire pipeline: it downloads the .deb from the GitHub release, sources a decrypted base IPA (from a local path or a service like armconverter.com), injects the tweak into the base app via cyan (pyzule-rw), optionally renames the bundle display name with --bundle-name, then continues through the normal sign-and-publish pipeline. The docs/SIDELOAD_WORKFLOW.md guide covers this in detail with step-by-step examples.

Device management includes sideload-register-device for adding iPhones (with USB auto-detection via libimobiledevice), sideload-remove-device for deregistration, sideload-remove for removing published apps from R2, and sideload-list for inspecting the current cert, device, and published app state.

Translation sync with Crowdin

The toolkit manages three parallel translation surfaces through Crowdin:

  • App Store metadata — locale YAML files under metadata/<slug>/locales/
  • Screenshot headlines — per-locale YAML under metadata/<slug>/screenshots/_content/
  • In-app strings — mirrored files under metadata/<slug>/app-strings/ for apps that define the app_strings block

A GitHub Action (crowdin-sync.yml) runs nightly downloads at 03:00 UTC and supports manual triggers for upload, download, seed-translations (with --auto-approve-imported and --import-eq-suggestions flags), or bidirectional sync. The action opens a localization PR with formatted commit messages and handles orphan file cleanup via the delete_orphan_file_ids workflow input.

Locale remapping handles the naming mismatch between Crowdin and Apple: Crowdin uses ar where Apple expects ar-SA, and zh-CN maps to zh-Hans. The sync workflow applies these mappings automatically. For in-app strings, the app-strings-sync command mirrors files between the app repository, the metadata directory, and Crowdin. The --direction push flag copies from the app repo into the translation pipeline; --direction pull brings translated strings back. The full configuration process is documented in CROWDIN_SETUP.md with ten sections covering project creation, token management, secret configuration, and troubleshooting.

Optional Streamlit dashboard

For operators who prefer a visual interface, ./apple ui launches an eight-page Streamlit dashboard covering apps overview, metadata editing and rendering, translation progress, screenshot composition and upload, TestFlight build inspection, release management, sideload state, and settings. The dashboard shells out to the same CLI backend, so .env and local state remain the single source of truth.

The dashboard is intentionally optional. It is not installed by requirements.txt — you need to install streamlit separately. This keeps the core CLI lightweight for CI/CD environments where a web UI makes no sense. A browser-level smoke test in the GitHub Actions pipeline verifies the dashboard boots without errors on every push.

Safety model and dry-run philosophy

Every command that can modify state exposes a --dry-run flag. This applies to certificate revocation, profile regeneration, metadata push, screenshot upload, build attachment, version submission, phased rollout control, and sideload operations. The recommended workflow is always: run with --dry-run first, review the output, then run for real.

Additional safety measures:

  • cleanup backs up stale provisioning profiles before removing them from the Xcode directory
  • metadata-push refuses to overwrite non-empty ASC values with empty rendered output
  • Template rendering uses StrictUndefined so missing variables fail immediately rather than producing silent blanks
  • Secrets (.env, .p8 keys, service account JSON, sideload certificates) stay local and are gitignored
  • Rendered payloads, backups, and cache directories are all gitignored
  • The test suite (15+ pytest files) covers non-network paths: screenshot device rules, metadata helpers, ASC baseline caching, sync state persistence, and UI preferences
  • GitHub Actions runs the full pytest suite plus a browser-level Streamlit smoke test on every push
  • The docs/TROUBLESHOOTING.md guide (1,600+ words) covers 20+ failure scenarios with symptom, fix, and root cause explanation for each

Testing and continuous integration

The test suite is designed around a clear boundary: anything that talks to ASC or requires macOS-specific tooling is excluded from CI. What remains is a practical set of 15+ test files covering screenshot device dimension rules, metadata rendering helpers, ASC baseline caching, sideload sync state persistence, UI preference storage, and the app catalog parser.

Two GitHub Actions workflows run on every push: tests.yml executes the full pytest suite on Python 3.11 and then boots the Streamlit dashboard in headless mode to verify it renders without errors (a Playwright-based smoke test). crowdin-sync.yml handles the nightly translation download and can be triggered manually for uploads or seeding.

CLI command reference

The complete command set organized by area:

AreaCommandPurpose
OverviewinventoryRead-only view of Keychain, local profiles, and ASC profiles
Overviewapps-listApp catalog with live ASC version and locale state
Overviewapps-auditFull audit: versions, localizations, review status, availability
Certslist-certsTeam certificates with SHA-1 fingerprints and expiry
Certsrevoke-certRevoke a certificate in ASC
Profileslist-profilesASC provisioning profiles with cert cross-references
Profilesregenerate-profilesRe-issue profiles after cert rotation
CleanupcleanupBack up stale profiles, delete expired Keychain identities
Metadatametadata-pullPull current app-info and version localization from ASC
Metadatametadata-renderRender Jinja2 templates into .rendered/
Metadatametadata-pushPush rendered metadata back to ASC
Translationsapp-strings-syncMirror in-app strings between app repo and Crowdin
Versionsversion-createCreate a new draft AppStoreVersion
Screenshotsscreenshots-validateValidate folder structure and Apple dimensions
Screenshotsframes-renderGenerate fallback device frames with Pillow
Screenshotsscreenshots-generateCompose localized screenshots from raw UI + frames + headlines
Screenshotsscreenshots-captureSimulator harness: Maestro flows, locale switching, raw capture
Screenshotsscreenshots-snapSingle-screen interactive capture for quick iterations
Screenshotsscreenshots-pushUpload to ASC via multipart S3 flow
Operationssales-reportFetch and aggregate ASC sales reports
Operationsreviews-listList customer reviews with response state
Operationsreviews-respondPost developer response to a review
Operationstestflight-listList TestFlight builds and beta groups
Operationstestflight-inviteCreate tester and invite to beta group
Releasebuild-attachAttach TestFlight build to editable version
Releaseversion-submitPre-flight checks and App Review submission
Releasephased-releaseInspect or control phased rollout
Sideloadsideload-initIssue a new sideload signing certificate
Sideloadsideload-import-certImport existing .p12 certificate for sideload
Sideloadsideload-register-deviceRegister iPhone for sideload (USB auto-detect)
Sideloadsideload-remove-deviceDeregister a device from sideload profile
Sideloadsideload-installFetch, re-sign, and publish IPA for OTA install
Sideloadsideload-removeRemove a published sideload app from R2
Sideloadsideload-listShow current cert, device, published app state
Sideloadsideload-tg-initBind Telegram chat for install link delivery
DashboarduiLaunch Streamlit dashboard

Getting started

The setup requires macOS, Python 3.10+, and an App Store Connect API key with the appropriate role (App Manager or Admin). Optional dependencies include Maestro for screenshot capture, zsign for sideload re-signing, libimobiledevice for USB device detection, the Crowdin CLI for local translation sync, and Playwright for browser smoke tests.

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# configure ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_PATH in .env

./apple apps-list --no-server    # verify catalog parses
./apple list-certs               # verify ASC authentication
./launch                         # interactive operator menu

The repository includes detailed setup and workflow guides: docs/SETUP.md covers end-to-end bootstrap with verification steps for each dependency, ios/API_KEY_SETUP.md covers App Store Connect credential creation with common failure modes, CROWDIN_SETUP.md covers translation sync configuration across ten sections, docs/METADATA_WORKFLOW.md documents the template precedence system and new-locale/new-app checklists, docs/SCREENSHOTS_WORKFLOW.md covers the full capture-compose-validate-upload pipeline, docs/SIDELOAD_WORKFLOW.md walks through the re-sign and publish process including tweak injection, docs/RELEASE_WORKFLOW.md documents the submission sequence, and docs/TROUBLESHOOTING.md covers 20+ failure scenarios with recovery procedures.

Current scope and what comes next

The iOS side is production-ready and covers the full App Store Connect lifecycle. The toolkit manages three active apps across four locales with automated metadata rendering, screenshot generation for both iPhone and iPad device classes, and release submission.

Google Play automation is the main gap. The infrastructure is ready: service account authentication is documented in android/SERVICE_ACCOUNT_SETUP.md, the ./droid <cmd> entry point is planned, and metadata/apps.yaml supports the Android config block. What’s missing is the CLI commands for track inspection, AAB upload, and promotion between tracks.

Other planned additions include a Figma-based screenshot import pipeline (screenshots-import-figma) for white-label RocketChat forks where the master Figma template uses brand variables for multi-brand screenshot generation, lint and type-check CI workflows, and packaging as an installable Python distribution.

FAQ

Does mobile-stores-cicd replace Fastlane? For small iOS portfolios, yes. It covers certificate management, metadata push, screenshot automation, release submission, and phased rollout. It does not cover Android yet, and it requires macOS for Keychain and Xcode integration.

Can I use this on CI/CD servers? The CLI and metadata operations work on any machine with Python 3.10+ and valid ASC credentials. Screenshot capture requires macOS with Xcode and Simulator. The test suite runs on GitHub Actions.

What is the sideload pipeline for? Re-signing third-party iOS apps with your own Apple Developer certificate, uploading to Cloudflare R2, and installing via OTA manifest on a registered iPhone. It also supports .deb tweak injection via cyan for modified apps.

How does the metadata templating differ from Fastlane? Fastlane uses flat text files per locale. This toolkit uses Jinja2 templates with five-level value precedence and StrictUndefined rendering. Shared boilerplate lives in one place while app-specific content is maintained per-app.

Is the sideload pipeline legal? Re-signing apps with your own Apple Developer certificate for personal use on your own device is within Apple’s developer program terms. The pipeline uses official ASC APIs for certificate and profile management.

What locales does the screenshot pipeline support? Any locale App Store Connect supports. The composition engine has automatic font fallbacks for Arabic and Chinese. The current production setup covers en-US, ru, ar-SA, and zh-Hans.

What happens if a template variable is missing? The renderer uses StrictUndefined mode, so a missing variable raises an error immediately. The metadata-push command also refuses to overwrite non-empty ASC values with empty rendered output as a second safety layer.

How does the Crowdin locale mapping work? The sync workflow automatically maps Crowdin ar to Apple ar-SA and Crowdin zh-CN to Apple zh-Hans. This remapping is applied during both upload and download.

Conclusion

mobile-stores-cicd started as a certificate rotation script and grew into a 37-command toolkit that handles the full iOS app lifecycle. The metadata templating system eliminates per-locale file duplication with a five-level value precedence and fail-fast rendering. The screenshot pipeline reduces a multi-hour manual process to a few CLI commands, producing device-framed, localized marketing screenshots across iPhone and iPad with automatic RTL and CJK font handling. The sideload subsystem solves a genuinely annoying problem (installing modified third-party apps on a non-jailbroken iPhone) with a clean one-command workflow that covers .deb tweak injection, code signing, R2 upload, and Telegram delivery.

The repository includes 21 documentation files covering every workflow from initial setup through troubleshooting, with detailed guides for API key creation, Crowdin configuration, and the sideload pipeline. The architecture and workflow patterns described here apply to any team managing a small iOS app portfolio through App Store Connect.

If you are dealing with the same App Store Connect overhead and want to discuss how this approach could work for your team, reach out for a consultation.

Ilia Arestov, Dubai International Airport Free Zone, Dubai, UAE

Rate article