App Release
Config Reference
This describes the app config consumed by:
app-release --config apps/<app>/release/ios.json
Top-Level Fields
{
"app": "chess",
"name": "Mazzi Chess",
"scheme": "Chess",
"project": "apps/chess/ios/Chess.xcodeproj",
"iosDir": "apps/chess/ios",
"productName": "Chess",
"bundleId": "app.mazzi.chess",
"appleId": "6785882144",
"releaseVersion": "1.0.0"
}
app: stable app slug, used for default paths.name: display name used by promo video defaults.scheme: Xcode scheme.project:.xcodeprojpath, relative to the product repo root unless absolute.iosDir: directory wherexcodegen generateandxcodebuildshould run. Defaults toapps/<app>/ios.productName: simulator app product name. Defaults toscheme.bundleId: iOS bundle identifier.appleId: App Store Connect app Apple ID.platform: optional.ios(default) ormacos. Selects the build/launch/capture pipeline. Omitting it keeps the existing iOS behavior, so other integrations are unaffected.releaseVersion: optional explicit output version. If omitted, the tool tries to readMARKETING_VERSIONfromiosDir/project.yml, then falls back tounversioned.
Platforms
{
"platform": "macos"
}
ios(default): builds for the iOS Simulator, launches withxcrun simctl, and captures withsimctl io screenshot. RequiresiosDir, XcodeGen, and a simulator.macos: builds forplatform=macOS, launches the.appdirectly with launch args + environment, and captures each on-screen window withscreencapture. Does not require a simulator,iosDir, or XcodeGen. Requiresxcodebuildandscreencapture.
Both platforms share the same screenshots config shape (devices, locales, color modes, scenarios, env, launch args). For macos, screenshots.devices defaults to [{ "name": "mac", "displayType": "mac" }] and env vars are passed to the app directly (no SIMCTL_CHILD_ prefixing).
Validation
{
"validation": {
"command": "cd apps/chess/ios && xcodebuild test -scheme Chess -destination 'platform=iOS Simulator,name=iPhone 17 Pro'"
}
}
Or:
{
"validation": {
"script": "apps/chess/ios/scripts/validate-local.sh"
}
}
command: shell command run from the repo root.script: executable path run directly.
If neither is set, the default script is:
apps/<app>/ios/scripts/validate-local.sh
Metadata
{
"metadata": {
"sourceDir": "apps/chess/release/app-store",
"locales": ["en-US"],
"deliverDir": "artifacts/app-store/chess"
}
}
sourceDir: folder containing<locale>.jsonmetadata files.locales: locale IDs to export.deliverDir: output root for Fastlane Deliver metadata.
Supported metadata JSON keys:
namesubtitlepromotional_textdescriptionkeywordsrelease_notessupport_urlprivacy_urlmarketing_urlcopyright
Screenshots
{
"screenshots": {
"outputDir": "artifacts/screenshots/chess",
"configuration": "Debug",
"quietBuild": true,
"defaultWaitSeconds": 2,
"eraseBeforeCapture": false,
"scenarioBudgetPerDevice": 10,
"baseLaunchArgs": ["--ui-testing"],
"statusBar": {
"time": "9:41",
"wifiBars": 3,
"cellularBars": 4,
"batteryState": "charged",
"batteryLevel": 100
}
}
}
outputDir: base output folder. Final screenshots go underscreenshots/<version>.configuration: Xcode build configuration. Defaults toDebug.quietBuild: pass-quiettoxcodebuild. Defaults totrue.defaultWaitSeconds: default wait after app launch before capture.eraseBeforeCapture: erase simulator before capture.scenarioBudgetPerDevice: max screenshot jobs per device.baseLaunchArgs: launch args used for every screenshot scenario. Defaults to["--ui-testing"].statusBar: values passed toxcrun simctl status_bar override.windowAppearTimeoutSeconds: macOS only. Seconds to wait for the app window to appear before capturing a window id. Defaults to15.
Screenshot Devices
{
"devices": [
{
"name": "iPhone 17 Pro Max",
"os": "26.2",
"displayType": "APP_IPHONE_67",
"createIfMissing": true,
"udid": "optional-existing-simulator-udid",
"simulatorName": "Mazzi Screenshots"
}
]
}
name: simulator device type name.os: iOS runtime version.displayType: App Store screenshot display type used for Deliver output names.createIfMissing: create the simulator if it does not exist. Defaults to true.udid: use an existing simulator directly.simulatorName: custom name when creating a simulator.
Locales
{
"locales": [
{ "id": "en-US", "appLanguage": "en" }
]
}
String form is also supported:
{
"locales": ["en-US"]
}
Color Modes
{
"colorModes": [
{
"id": "light",
"launchArgs": ["--color-mode", "light"],
"env": { "MAZZI_SCREENSHOT_COLOR_MODE": "light" },
"userDefaults": { "colorMode": "light" }
}
]
}
Color modes can add launch args, environment variables, and user defaults.
Scenarios
{
"scenarios": [
{
"id": "settings",
"filename": "02-{deviceSlug}-settings-{colorMode}.png",
"enabled": true,
"waitSeconds": 2,
"launchArgs": ["--screenshot", "settings"],
"env": { "MAZZI_SCREENSHOT_ROUTE": "settings" },
"userDefaults": { "hasSeenOnboarding": true },
"colorModes": ["light", "dark"],
"locales": [{ "id": "en-US", "appLanguage": "en" }],
"levels": [1, 2, 3],
"levelLaunchArgs": ["--level", "{level}"]
}
]
}
Template values supported in launch args, env, and filenames:
{locale}{appLanguage}{app}{colorMode}{scenario}{level}{device}{deviceName}{deviceSlug}
Promo Video
{
"promoVideo": {
"deviceName": "iPhone 17 Pro Max",
"width": 1290,
"height": 2796,
"outputDir": "artifacts/videos/chess",
"outputLayout": "default",
"locale": "en-US",
"eraseBeforeCapture": false,
"recordings": [],
"segments": []
}
}
deviceName: screenshot device to use.device: inline device object if not usingdeviceName.width/height: rendered video size.outputDir: base video output folder. Final videos go undervideos/<version>.outputLayout:defaultwritesvideos/<version>/*.mp4;app-previewswrites<version>/<device>/*-<locale>.mp4.locale: locale suffix used by theapp-previewsfilename. Defaults toen-US.eraseBeforeCapture: erase simulator before capture.recordings: real app walkthrough recordings with explicit durations.segments: card/capture compositions; must total 10 seconds.
Recording entries can set:
iddeviceNameordevicefilenamewidth/heightdurationSecondsstartDelaySecondslaunchArgsenvuserDefaultsfilterorvideoFilterfor FFmpeg scaling/croppingcursor.movesandcursor.clicks
Cursor coordinates can be absolute pixels or relative values from 0 to 1.
Set cursor.enabled to false to render click indicators without a visible moving cursor.
Supported segment types:
{ "type": "title", "durationSeconds": 1.2, "title": "Mazzi Chess", "subtitle": "Classic chess, offline." }
{ "type": "card", "durationSeconds": 1.2, "title": "Eight levels", "subtitle": "From Noob to Grandmaster." }
{ "type": "cta", "durationSeconds": 1.6, "title": "Mazzi Chess", "subtitle": "Available on the App Store" }
{ "type": "capture", "durationSeconds": 3.0, "scenarioId": "opening" }
Capture segments can also set:
localecolorModelevellaunchArgsenvuserDefaultsstartDelaySeconds
Archive
{
"archive": {
"configuration": "Release",
"archivePath": "/tmp/mazzi-chess/Chess.xcarchive",
"exportPath": "/tmp/mazzi-chess/export",
"ipaPath": "/tmp/mazzi-chess/export/Chess.ipa",
"exportOptionsPlist": "apps/chess/ios/ExportOptions.plist",
"allowProvisioningUpdates": false
}
}
configuration: Xcode build configuration. Defaults toRelease.archivePath: target.xcarchivepath.exportPath: IPA export folder.ipaPath: explicit IPA path.exportOptionsPlist: enables IPA export after archive.allowProvisioningUpdates: passes-allowProvisioningUpdates.
Upload
{
"upload": {
"ipaPath": "/tmp/mazzi-chess/export/Chess.ipa",
"deliverDir": "artifacts/app-store/chess",
"appleId": "6785882144",
"apiKeyEnv": "APP_STORE_CONNECT_API_KEY_ID",
"apiIssuerEnv": "APP_STORE_CONNECT_API_ISSUER_ID"
}
}
ipaPath: IPA to upload. Defaults to archive output.deliverDir: Fastlane Deliver output root.appleId: App Store Connect Apple ID. Falls back to top-levelappleId.apiKeyEnv: environment variable containing the App Store Connect API key ID.apiIssuerEnv: environment variable containing the App Store Connect issuer ID.
Multi-App Manifest
{
"apps": [
{ "slug": "chess", "releaseConfig": "apps/chess/release/ios.json" },
{ "slug": "reversi", "releaseConfig": "apps/reversi/release/ios.json" }
]
}
slugorid: app identifier passed to--app.releaseConfigorconfig: path to the app release config.
Relative config paths resolve from the manifest file location.