storebound/skills/adhoc-distribution/SKILL.md

6.1 KiB

name description
adhoc-distribution Get a test build of a mobile app or game onto specific devices without a store review - Apple ad-hoc / release-testing (register device UDIDs, an ad-hoc provisioning profile, ExportOptions.plist, xcodebuild -exportArchive) and Google Play internal app sharing (upload an AAB or APK, get a share link). Explains when to use ad-hoc vs TestFlight vs Play internal testing vs internal app sharing, the 100 devices per year limit, and the tester setup. Use when the user wants an IPA for a few devices, asks how to register a device or UDID, needs an ExportOptions.plist, or wants a quick Play install link for a build.

Ad-hoc builds and internal app sharing

CLI: storebound (or bunx storebound in a project that has it, or bun <toolchain>/cli/index.ts).

Which channel

Channel Who installs Review Limits Use it for
TestFlight internal (testflight skill) up to 100 App Store Connect users none 90-day builds the team, every build
TestFlight external up to 10,000 testers, by email or public link Beta App Review for the first build of a version 90-day builds wider beta
Apple ad-hoc / release-testing registered devices only none 100 devices per product family per membership year a client demo, a device lab, testers without App Store Connect access
Play internal testing track (play-first-release skill) up to 100 testers by email list none needs a track release the team, every build
Play internal app sharing anyone allowed to use the link (default: anyone with it) none 100 users per link, link valid 60 days a quick build for one person, any version code, debuggable builds

Prefer TestFlight and Play internal testing. Ad-hoc costs device slots you only get back at the start of a membership year; internal app sharing builds cannot be promoted to any track.

Apple ad-hoc

Config: apple.teamId (Membership details), apple.bundleId, optional apple.adhoc: method (release-testing, the name since Xcode 15.3, default; ad-hoc for older Xcode), devices (default .storebound/devices.csv), profile (a profile name: manual signing; unset: automatic signing).

  1. The owner collects the devices. UDIDs are personal data: they stay in .storebound/devices.csv (gitignored), never in chat, issues or commits. Header line name,udid,platform, one device per line, platform IOS or MAC_OS. The tester finds the UDID in Finder (iPhone connected, click the line under the device name) or Xcode Devices; an Apple silicon Mac needs its Provisioning UDID (System Information > Hardware).
  2. storebound apple adhoc devices (read-only): what is registered, UDIDs masked, and the slots used per device class.
  3. storebound apple adhoc register --dry: validates the CSV (40 hex digits, or 8 hex - 16 hex), compares with the registered devices and lists the new ones. Show it to the owner. Every new device uses one of the 100 yearly slots of its product family, also if it is disabled later. Then storebound apple adhoc register asks and registers. One device: --name "Test iPhone" --udid <udid> --platform IOS. It never disables, enables or deletes a device: that is the owner's call on the Apple Developer website.
  4. Signing, either:
    • automatic (no apple.adhoc.profile): Xcode creates the profile during export (-allowProvisioningUpdates), or
    • manual: storebound apple adhoc profile --dry, then storebound apple adhoc profile. It creates an IOS_APP_ADHOC profile for the bundle id with every enabled iOS device and the valid Apple Distribution certificates, and saves build/adhoc/<name>.mobileprovision (double-click to install). A profile cannot be changed: when devices were added, a new profile with a dated name is created; set apple.adhoc.profile to it. Deleting the old one is the owner's step on the developer website.
  5. Archive in Xcode (Product > Archive) or xcodebuild archive.
  6. storebound apple adhoc options --dry shows the ExportOptions.plist (method, teamID, signingStyle, thinning <none>, destination export); without --dry it writes build/ExportOptions.plist.
  7. storebound apple adhoc export --archive build/App.xcarchive --dry prints the exact xcodebuild -exportArchive command; without --dry it runs it and lists the .ipa in build/adhoc/. With -allowProvisioningUpdates Xcode may contact Apple to create or refresh signing assets.
  8. Install: Apple Configurator, Finder (drag the IPA onto the device) or Xcode Devices. A device that is not in the profile cannot install the build: re-run steps 3, 4 and 7.

Google Play internal app sharing

  1. Owner, once in Play Console > Internal app sharing (under Test and release): who may upload (the service account needs "Release apps to testing tracks" for the app, or a place on the uploader list) and who may download (an email list, or "Anyone you shared the link with can download", the default).
  2. storebound play share --aab app-release.aab --dry (or --apk): file, size, sha256 and package; nothing sent.
  3. storebound play share --aab app-release.aab asks, uploads and prints the share link and the certificate fingerprint (Google re-signs the build with an internal app sharing key). --json: { downloadUrl, certificateFingerprint, sha256 }. Limits: app bundles up to 10 GiB, APKs up to 1 GiB.
  4. Each tester, once per device: Play Store > Settings > About > tap "Play Store version" 7 times > turn on Internal app sharing. Then open the link on the device. Testers must be able to see the app's store listing on Google Play (country and track access). The link expires 60 days after the upload; re-upload for a new one.

The owner decides

  • Which devices to register (yearly slots) and removing or disabling any device.
  • Who may download internal app sharing links, and sharing a link at all (anyone with it can install the build).
  • Deleting old provisioning profiles.

Out of scope

  • Enterprise (in-house) distribution, Developer ID / notarized Mac apps, over-the-air install pages (the ExportOptions manifest key).
  • Firebase App Distribution and other third-party beta services.