---
title: iOS release
description: Maintainer guide to the Pipedash iOS release. Covers files, versions, workflows, secrets, App Review demo access, signing, disabled features, tests and App Store screenshots.
---

The iOS app is the `crates/pipedash-desktop` Tauri app built for iOS. Every build generates the Xcode project, and Git ignores `crates/pipedash-desktop/gen`. Everything iOS needs lives in the `ios` directory or in `crates/pipedash-desktop/Info.ios.plist`. The [Mobile](/docs/deployment/mobile) page describes the app from the user side.

## Files

File paths link to the `main` branch on GitHub.

| Path | What it is |
|---|---|
| [`ios/fastlane/Fastfile`](https://github.com/hcavarsan/pipedash/blob/main/ios/fastlane/Fastfile) | Build, simulator capture, screenshot export and upload, and release lanes |
| [`ios/xcode/PrivacyInfo.xcprivacy`](https://github.com/hcavarsan/pipedash/blob/main/ios/xcode/PrivacyInfo.xcprivacy) | Privacy manifest copied into the app target |
| [`ios/xcode/pipedash.entitlements`](https://github.com/hcavarsan/pipedash/blob/main/ios/xcode/pipedash.entitlements) | App entitlements without APNs access |
| [`ios/fastlane/metadata/en-US/release_notes.txt`](https://github.com/hcavarsan/pipedash/blob/main/ios/fastlane/metadata/en-US/release_notes.txt) | The "What's New" text that `release` sends |
| [`ios/fastlane/metadata/en-US/description.txt`](https://github.com/hcavarsan/pipedash/blob/main/ios/fastlane/metadata/en-US/description.txt) | The app description that `release` sends, including the foreground-refresh limits |
| [`ios/fastlane/metadata/en-US/support_url.txt`](https://github.com/hcavarsan/pipedash/blob/main/ios/fastlane/metadata/en-US/support_url.txt) | The support URL that `release` sends. It points to the [support page](/support), which lists an email contact |
| [`ios/fastlane/review_notes.txt`](https://github.com/hcavarsan/pipedash/blob/main/ios/fastlane/review_notes.txt) | App Review instructions, with the demo token, repository and workflow filled in at release time |
| [`ios/fastlane/app_review_demo.rb`](https://github.com/hcavarsan/pipedash/blob/main/ios/fastlane/app_review_demo.rb) | Read-only GitHub access checks before release signing or submission |
| [`ios/fastlane/ios_project.rb`](https://github.com/hcavarsan/pipedash/blob/main/ios/fastlane/ios_project.rb) | App version, disabled-feature checks and export profile setup |
| [`crates/pipedash-desktop/icons/ios/app-icon-1024.png`](https://github.com/hcavarsan/pipedash/blob/main/crates/pipedash-desktop/icons/ios/app-icon-1024.png) | Full-bleed App Store icon without alpha |
| [`ios/app-store-screenshots`](https://github.com/hcavarsan/pipedash/blob/main/ios/app-store-screenshots) | Screenshot editor, saved decks, mock captures and export scripts |
| `ios/fastlane/screenshots/en-US/` | Generated iPhone and iPad images for upload. Git ignores this directory |

## Versions

- **App version:** the release version with the major number raised by 1, because the App Store already shipped `1.0`. Release `v0.4.0` ships as `1.4.0`. `mise run bump:*` keeps working, and the Fastfile derives the iOS version.
- **Build number:** the latest build in App Store Connect plus 1.
- **Minimum iOS:** `15.0`. The value `bundle.iOS.minimumSystemVersion` in [`tauri.ios.conf.json`](https://github.com/hcavarsan/pipedash/blob/main/crates/pipedash-desktop/tauri.ios.conf.json) sets it. Tauri maps it to `IPHONEOS_DEPLOYMENT_TARGET`. The schema of the installed Tauri CLI also defaults to `15.0`.

## Workflows

Both workflows run on the `macos-26` runner.

- **`ios-check.yml`:** runs on pull requests that touch the app and supports manual runs on a feature branch. The first job runs the Ruby helper tests and publishing guards. It builds an unsigned IPA and checks the disabled background capabilities, the `Info.plist` keys, export compliance, the app icon and the required-reason APIs. The second job builds for the simulator, starts the app on iPhone and iPad and uploads screenshots as the `ios-simulator-screenshots` artifact.
- **`ios.yml`:** runs the `beta` lane (TestFlight) on every `v*` tag. Manual runs also offer `release` (App Review submission with screenshots) and `upload_screenshots` (screenshots only, no submission). A manual `beta` run accepts a `beta_ref` branch to build. The other lanes reject it. The workflow uses the `app-store` environment, which only allows `main` and `v*` tags. Screenshot runs keep the generated PNGs and the full bundles in the `ios-app-store-screenshots` artifact for 14 days.

## Secrets and variables

These values belong to the `app-store` environment unless noted.

| Name | Kind | Used by |
|---|---|---|
| `MATCH_PASSWORD`, `MATCH_DEPLOY_KEY` | Secret | `match` reads the certificate and profile from `pipedash-certificates-v2` |
| `ASC_KEY_ID`, `ASC_ISSUER_ID`, `ASC_KEY_CONTENT` | Secret | App Store Connect API key |
| `APPLE_TEAM_ID` | Repository secret | Signing team |
| `APP_REVIEW_DEMO_TOKEN` | Secret | `release` only: GitHub token for the demo repository, filled into the review notes |
| `APP_REVIEW_DEMO_REPOSITORY` | Variable | `release` only: `owner/repo` of the demo repository |
| `PIPEDASH_PUSH_RELAY_URL` | Optional repository variable | Cannot activate remote push or Watch in this build |

## App Review demo access

`release` checks the configured GitHub demo before Apple authentication, signing or upload. It sends only authenticated `GET` requests. The repository must be public, active and writable by the user of the token. Public access lets reviewers open its GitHub page without a repository invitation. GitHub can still require a sign-in to read workflow logs in the browser.

The check finds an active `workflow_dispatch` workflow on the default branch with defaults for required inputs, a successful manual run and available jobs. It reads the first 100 workflows at most, which matches the workflow list of the app. It fills the name of that workflow into the review instructions.

An HTTP failure stops the release with the endpoint and permission guidance. A network failure omits raw exception details to protect credentials. The lane registers the token for secret masking and rejects `--verbose`, because the HTTP debug output of Fastlane can include request credentials.

Complete this checklist before each submission:

1. Use a dedicated public demo repository. Keep production code, credentials, organization secrets, deployments and production network access out of its workflows.
2. Create a fine-grained token with "Only select repositories" set to that repository. Grant Actions read/write and Contents read only. Do not use a production token or an unrestricted classic token. Set an expiry of at least 90 days, so the token stays valid through re-reviews and appeals. `release` checks the token only when it runs, so an expired token later leaves App Review unable to add the demo provider.
3. Keep a harmless `workflow_dispatch` workflow on the default branch with defaults for any required inputs. Give it enough runtime to test "Stop workflow". Keep a successful manual run with jobs.
4. Test the review instructions on an iPhone or iPad. Cover provider setup, run details, browser logs, "Run workflow" > "Trigger" and "Stop workflow". The provider export must open the native Files picker and allow any destination.
5. Answer the App Store Connect export compliance questions for the build. The app links rustls and ring for TLS and uses AES-256-GCM with Argon2id for password-encrypted credential storage. `ITSAppUsesNonExemptEncryption` is `false` in `Info.ios.plist`, and `verify_ipa` demands that value. Keep it only if the export classification shows that this use is exempt. Otherwise set it to `true`, update `verify_ipa` and upload any documentation that App Store Connect requests.

GitHub read endpoints cannot prove the repository restriction of the token, the Actions write permission or that the workflow is harmless. The `permissions.push` field of the repository reflects the permission of the user. It does not reflect the full permissions of the token. The preflight does not dispatch or cancel a workflow to test write access, so it does not replace the checklist.

## Run locally

Run the checks on macOS:

```bash
cd ios
bundle install
bundle exec fastlane ios check
```

The certificate and the profile expire on 2027-10-24. Renewing them runs `match` without `readonly` from the `ios` directory with the App Store Connect API key. The history of `pipedash-certificates-v2` shows earlier renewals.

## Signing prerequisites

The signed lanes use read-only `match` for `com.henrique.pipedash` only. The certificate repository must contain its App Store profile and the private key of the certificate. The app does not request the Push Notifications entitlement, and `verify_ipa` rejects an APNs entitlement in the signed app.

An authorized maintainer renews the app profile from macOS with writable access to the certificate repository:

```bash
cd ios
bundle exec fastlane match appstore --app_identifier com.henrique.pipedash --readonly false
```

The command uses the existing authentication of the certificate repository and `MATCH_PASSWORD`. The Apple account or API key needs permission to manage identifiers and distribution profiles. The CI deploy key and the read-only `match` step do not create missing profiles.

Tauri archives the app with manual signing settings. Fastlane exports the archive with the provisioning profile mapping of the app. The `build_app` action requires `export_method: "app-store"`. The Tauri CLI and the Xcode `export_options[:method]` value keep `app-store-connect`. `verify_ipa` checks the disabled-feature metadata and rejects bundled app extensions as part of its App Store checks.

## Disabled build watching

The release build disables Watch, remote push and Live Activities. `PipedashBuildWatchEnabled` is `false` in `crates/pipedash-desktop/Info.ios.plist`. The native plugin rejects watch starts, skips background task registration and reports Watch as unsupported. The frontend hides the Watch and remote push controls. Existing snapshots stay stored and do not restart automatically.

The app declares no build-watch background task identifiers and no `processing` or `remote-notification` background modes. It declares only the `com.henrique.pipedash.notifications-refresh` task with the `fetch` mode, and `verify_ipa` requires exactly that. The app bundles no WidgetKit extension and no Live Activity support. `verify_ipa` rejects these capabilities and any enabled build-watch flag before upload.

A relay URL in the environment cannot activate either feature. Changing the flag alone is not a supported release configuration.

On upgrade, the native plugin dismisses custom activities that older versions left behind. It keeps their attribute type only for this cleanup. The disabled monitoring code uses iOS 26 APIs behind availability checks, so it does not change the iOS 15.0 minimum.

### Remote push

Remote push uses the same `PipedashBuildWatchEnabled` flag. The app skips token synchronization and rejects the pairing, unpairing and test push commands before it reads saved settings or uses the network. The native plugin unregisters from remote notifications at app start and rejects registration without asking for notification permission.

Saved pairings stay on the device. Local notifications work independently and need no APNs entitlement. The push APIs of the server stay separate from this disabled iOS feature. The [push relay page](/docs/resources/push-relay) covers the server side.

## Tests

Run the Ruby tests from the `ios` directory:

```bash
bundle exec ruby -e 'Dir["fastlane/*_test.rb"].sort.each { |file| require_relative file }'
```

The `ios-check.yml` workflow runs this suite before the build. The App Review preflight cases run offline, without GitHub or Apple credentials:

```bash
bundle exec ruby fastlane/app_review_demo_test.rb
```

The screenshot tests use temporary files and replace the external Apple actions. They check the export failure gates, locale isolation, screenshot-only delivery and TestFlight isolation. The delivery checks exercise the Fastlane runner without authentication or upload.

## App Store screenshots

The screenshot editor lives in [`ios/app-store-screenshots`](https://github.com/hcavarsan/pipedash/blob/main/ios/app-store-screenshots). It edits the English iPhone and iPad decks without changing the production app. Commit its project JSON and the source PNGs after the compositions get approval. `release` exports those saved compositions with their device mockups. It does not recapture the app and does not replace approved source images. The [screenshot editor maintenance page](/docs/resources/app-store-screenshots) covers headline alternatives, dependency upkeep and the editor tests.

### Editor

The editor needs Node.js `^22.22.3 || >=24.8.0` and Bun. Run it from `ios/app-store-screenshots`:

```bash
bun install --frozen-lockfile
bun dev --hostname 127.0.0.1 --port 3000
```

Open `http://127.0.0.1:3000`. Select iPhone or iPad, edit the copy or drag the device, and click "Export bundle". Each deck has 5 framed product captures and a feature-list closer. Changes save to `app-store-screenshots.json`. Keep that file and `public/screenshots/` together.

### Design rules

The customized `midnight-glow-pro` preset appears as "Pipedash Flat" in the editor. It follows the [Pipedash design guide](https://github.com/hcavarsan/pipedash/blob/main/DESIGN.md): system typography, solid surfaces, quiet separators and blue emphasis. The decks have no decorative beams, halos, textures or glass cards. The first 2 slides share a thin baseline across the connected canvas. Headlines and product content stay inside their own crops.

Palette values come from `src/theme/index.ts` and the iOS surface roles in `src/App.css`. The theme entry in `ios/app-store-screenshots/src/lib/constants.ts` holds the screenshot palette: the iOS cell background, gray foregrounds and blue shade 3 for text emphasis. Shade 7 is the accent on light backgrounds. Captured UI keeps the status tokens of the app.

The template supplies the physical iPhone and iPad bezels. Apple licenses those assets for Apple-platform app mockups. The Android and Mac decks stay empty.

### Captures

The images show the real Pipedash frontend with synthetic repositories, runs, provider incidents and notifications. They are Chromium captures with iOS user agents, not native iOS simulator captures. They contain no real credentials or customer data. The capture script allows only a loopback app URL, blocks external HTTP requests and rejects unexpected API calls or mutations.

From the repository root, start the application frontend:

```bash
bun install --frozen-lockfile
bun run dev --host 127.0.0.1 --port 5198 --strictPort
```

In another terminal, from the repository root, run the capture script:

```bash
node ios/app-store-screenshots/scripts/capture-app.mjs
```

The script uses the Playwright dependency of the editor and `/usr/bin/chromium`. `CHROMIUM_PATH` selects another Chromium executable. `CAPTURE_APP_URL` accepts another loopback frontend URL. The script checks the output dimensions, the populated workflow inputs, unexpected requests and page errors.

Captures live in `public/screenshots/apple/{iphone|ipad}/en/01.png` through `05.png`, in this order:

1. Unified pipelines across GitHub, GitLab, Jenkins and Argo CD.
2. Workflow execution with release parameters.
3. Failed workflow and run history.
4. Provider health and an incident from its status page.
5. Failure, outage and recovery notifications.

Reload the editor after a recapture, so it reads the updated images. The source sizes are `1320x2868` for iPhone and `2064x2752` for iPad.

### Exports and sizes

The `exports/` directory contains the generated bundles and thumbnail previews. Git ignores it, because the editor can regenerate it.

| Deck | Slides | Export sizes | PNGs |
|---|---|---|---|
| iPhone | 6 | `1320x2868`, `1284x2778`, `1206x2622`, `1125x2436` | 24 |
| iPad | 6 | `2064x2752`, `2048x2732` | 12 |

`bun run typecheck` and `bun run build` check the editor. `bun run test:export-fastlane` runs the export edge tests. The export checks cover exact dimensions, opaque RGB pixels and solid corner pixels.

Browser captures do not show the native system bars or the WebKit rendering. Compare them with the shipped app and check the final files in App Store Connect before submitting.

### Publishing with Fastlane

From `ios`, `bundle exec fastlane ios prepare_screenshots` builds the editor and exports both saved decks without Apple credentials. It installs Playwright Chromium unless `CHROMIUM_PATH` points to an existing browser. On Linux, `CHROMIUM_PATH=/usr/bin/chromium bundle exec fastlane ios prepare_screenshots` uses the installed browser.

The script drives the export through the same editor controls. Autosaves stay in browser memory. The validation checks that the canonical JSON stays unchanged and that both decks are complete before it replaces `fastlane/screenshots/en-US/`. Supported decks use English, portrait orientation and 1 to 10 slides each. Preparation rejects extra directories in the upload folder instead of sending them.

The upload directory holds one size per device, `1320x2868` for iPhone and `2064x2752` for iPad. Uploading both exported iPad sizes would put 12 images into one Fastlane device slot, which is above the Apple limit of 10 images. The complete multi-size bundles stay in `app-store-screenshots/exports/`.

To update only the screenshots on the existing editable iOS version, run:

```bash
bundle exec fastlane ios upload_screenshots
```

This lane needs `ASC_KEY_ID`, `ASC_ISSUER_ID` and `ASC_KEY_CONTENT`. It needs no signing or `match` credentials. It skips the binary, the metadata, the version update and the review submission.

`release` includes the same images in the App Store submission flow, and `beta` leaves App Store screenshots alone. Both publishing lanes replace all existing screenshot sets for `en-US` and leave other locales unchanged. A failed export stops either lane before Apple authentication or upload. The existing `screenshots` lane remains the native simulator first-start check.
