Guides
Distribution
Get a build onto testers' devices with a link they can tap — and register the release and upload its symbols in the same step, so crashes from that build are readable from the moment it's installed.
Over-the-air install links
When you upload an .ipa, Pharen returns an install link — an itms-services URL that installs the app over the air. Open it in Safari on the iPhone and the app installs directly, no App Store, no cable:
itms-services://?action=download-manifest&url=https://install.pharen.ai/install/<opaque>/manifest.plistInstall links live at an unguessable path — they're shareable and bookmarkable, but not enumerable, so one organization's builds can't be discovered from another's. The link is what you send a tester; installing needs no login.
The build itself is never parked at a public address. When a tester opens the link, Pharen mints a short-lived signed URL for that one download and renders the manifest around it on the spot — so an install link that leaks doesn't hand anyone a permanent copy of your unreleased binary.
The whole release in one command
The canonical release lane is a single shell script. From your app directory:
bash release/release-lane.shIt runs the entire chain and prints the install link at the end:
- archive the app and export an IPA;
symbols upload— upload every dSYM the archive produced, so the release's crashes symbolicate. This runs first, so no later failure can skip it;releases create --commit— register the release with the commit it was built from;builds upload— upload the IPA and mint the OTA install link.
A build is one commit, so the lane refuses a working tree with uncommitted or untracked changes (ignored files don't count): such a file can reach the archive, and then no commit is the build's source. It checks again after generating the project and after the archive, and stops before registering or distributing if the tree changed while it ran. If your CI writes files it doesn't ignore, set PHAREN_ALLOW_DIRTY=1: the lane builds and distributes as usual but registers no commit. Outside a git checkout (with PHAREN_BUILD_NUMBER set) there is no commit to record, and the lane says so.
Nothing app-specific is baked into the script: version, build number, and bundle id are read from the archived Info.plist (so uploaded metadata can never drift from what was built), and your org/app come from a committed .pharen.yml. On your Mac, sign in once with pharen auth login; for CI, see Running it in CI. No credential is ever written into your repo.
Getting the lane into your repo
pharen init ios writes the lane (and a .pharen.yml) into your project — no network calls, no prompts:
pharen init ios --org your-org --app your-app-idIt writes .pharen.yml at the repo root and copies release/release-lane.sh plus release/exportOptions.plist into your app, filling in your Apple team id when it can find one. The release lane and init use the pharen CLI (npm install -g @pharen-ai/cli); your credentials come with an approved application. See the CLI reference.
Before the first run
- Add a
Stagingbuild configuration. The lane builds it by default: a copy of Release that carries your test ingest key, so testers' sessions stay out of your live numbers. Named yours differently? SetPHAREN_CONFIGURATION. - Register your testers' devices. An install link carries an ad-hoc build, which installs only on devices in its provisioning profile.
pharen device-requests listprints the devices waiting to be added, ready to paste into the Apple Developer portal.
Running it in CI
The lane runs unattended on a macOS runner. It needs:
- A Pharen credential. On GitHub Actions and GitLab CI, the job signs in with its own identity, so there is no secret to store. An organization owner binds the repository once (below). On other CI, export
PHAREN_AUTH_TOKENas a secret. - Full git history, since the build number is the commit count — or set
PHAREN_BUILD_NUMBER. - Code signing. Install your signing certificate on the runner. To let Xcode update provisioning profiles unattended, also set
PHAREN_ASC_KEY_PATH,PHAREN_ASC_KEY_IDandPHAREN_ASC_ISSUER_IDto an App Store Connect API key. - XcodeGen, if your project is generated from a
project.yml.
Bind a GitHub repository once, as an organization owner:
gh api repos/OWNER/REPO --jq '.id, .owner.id' # the two numeric ids
pharen workload federations add --kind github --project <repo-id> --project-owner <owner-id> --environment testThen a workflow like this ships a tester build on demand:
name: beta
on: workflow_dispatch
permissions:
contents: read
id-token: write # lets the job sign in to Pharen as itself
jobs:
beta:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install -g @pharen-ai/cli
# Install your signing certificate here.
- run: bash MyApp/release/release-lane.shOn GitLab CI, bind the exact group/project path instead, and run the job on a protected branch. A GitLab ID token signs in once and the lane runs up to five pharen commands, so the job declares five tokens and the lane hands each command the next one. It checks they are all there before it builds.
pharen workload federations add --kind gitlab --project acme/app --environment testbeta:
tags: [macos] # your macOS runner's tag
when: manual
id_tokens:
PHAREN_ID_TOKEN_1: { aud: 'https://api.pharen.ai' }
PHAREN_ID_TOKEN_2: { aud: 'https://api.pharen.ai' }
PHAREN_ID_TOKEN_3: { aud: 'https://api.pharen.ai' }
PHAREN_ID_TOKEN_4: { aud: 'https://api.pharen.ai' }
PHAREN_ID_TOKEN_5: { aud: 'https://api.pharen.ai' }
variables:
GIT_DEPTH: 0 # full history: the build number is the commit count
script:
- npm install -g @pharen-ai/cli # the runner needs Node 20 or later
# Install your signing certificate here.
- bash MyApp/release/release-lane.shA binding that reaches live must also prove the job runs on a branch not everyone can push: on GitHub, a deployment environment (--job-environment) that the job declares as its environment:; on GitLab, an exact protected ref (--ref). That way a branch anyone can push never ships to your customers. The other options are in the CLI reference.
TestFlight and the App Store
A store build is its own Release archive, carrying your live key:
bash release/release-lane.sh --app-storeThe lane refuses an archive that carries a test key, uploads its dSYMs and registers the release, then stops and prints the archive's path. Upload that archive to App Store Connect — from Xcode's Organizer (Distribute App) or your CI's own upload step — so the symbols Pharen holds match the build your users run. Store builds get no install link.
How TestFlight stays out of your customer metrics is on Environments & release tracks.