Workflow Files
What Mainline expects in your .github/workflows/ YAML — and what you can leave alone.
Overview
Mainline doesn’t generate or own your workflow YAML — your CI pipeline stays under your control. But Mainline’s CI features (Trigger Build, secret injection, live run monitoring) have a few baseline assumptions about how the workflow is shaped. This page lays them out.
If you don’t have a workflow yet, the test_app and sandbox repositories used by Mainline’s own App Review process are good copy-paste starting points.
Required structure
A workflow that Mainline can drive end-to-end has four parts:
- A push trigger on the branch you ship from (typically
main). - A macOS runner with the iOS SDK you build against — currently
macos-26for iOS 26. - A signing step that imports a P12 from secrets into a temporary keychain.
- An upload step that calls
xcrun altool --upload-appwith App Store Connect API credentials.
A minimal example:
name: Build & Deploy to TestFlight
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: macos-26
timeout-minutes: 12
steps:
- uses: actions/checkout@v4
- name: Install certificate
env:
P12_BASE64: $
P12_PASSWORD: $
run: |
echo "$P12_BASE64" | base64 --decode > /tmp/certificate.p12
security create-keychain -p "" build.keychain
security import /tmp/certificate.p12 -k build.keychain \
-P "$P12_PASSWORD" -T /usr/bin/codesign -f pkcs12
security list-keychains -s build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "" build.keychain
security set-key-partition-list -S apple-tool:,apple: \
-s -k "" build.keychain
- name: Install provisioning profile
env:
PROFILE_BASE64: $
run: |
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
echo "$PROFILE_BASE64" | base64 --decode > \
~/Library/MobileDevice/Provisioning\ Profiles/app.mobileprovision
- name: Build archive
run: |
xcodebuild -scheme MyApp \
-configuration Release \
-archivePath /tmp/MyApp.xcarchive \
-destination "generic/platform=iOS" \
-sdk iphoneos \
CODE_SIGN_STYLE=Manual \
CURRENT_PROJECT_VERSION=$GITHUB_RUN_NUMBER \
archive
- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath /tmp/MyApp.xcarchive \
-exportPath /tmp/MyApp-Export \
-exportOptionsPlist ExportOptions.plist
- name: Upload to TestFlight
if: $
env:
ASC_KEY_ID: $
ASC_ISSUER_ID: $
ASC_KEY_CONTENT_BASE64: $
run: |
mkdir -p ~/.appstoreconnect/private_keys
echo "$ASC_KEY_CONTENT_BASE64" | base64 --decode > \
~/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8
xcrun altool --upload-app \
--type ios \
--file /tmp/MyApp-Export/MyApp.ipa \
--apiKey "$ASC_KEY_ID" \
--apiIssuer "$ASC_ISSUER_ID"
See Secrets naming for what each secrets.* reference means and how Mainline maps to whichever names your team prefers.
You don’t need workflow_dispatch:
Earlier versions of Mainline required workflows to declare on: workflow_dispatch: so the Trigger Build button could fire them. That’s no longer the case. Mainline now triggers builds by either re-running the last completed run (clean) or pushing an empty commit (fallback). Both work against any workflow that has ever fired on push.
If you’d like manual dispatch wired into the workflow itself anyway — for example to run from GitHub’s web UI — adding workflow_dispatch: is fine, but Mainline doesn’t depend on it.
workflow_dispatch?The dispatch endpoint requires the workflow to opt in, and surfaces an unactionable 422 error when it doesn't. Falling back to "rerun or empty commit" works regardless and avoids ever editing your workflow file under you.
The [skip upload] marker
When you push commits that compile-test something but you don’t want to consume a TestFlight upload slot — for example, when you’re iterating on workflow YAML itself — include [skip upload] anywhere in the commit message. The if: clause on the upload step skips altool while still building and signing.
git commit -m "ci: fix indentation in build step [skip upload]"
Two things to watch:
GitHub Actions' contains() check matches the entire head_commit.message, including the body. If you describe a previous skip-upload commit in a later commit's body, that later commit will also skip upload. Avoid quoting the literal phrase in commit messages that you do want to upload.
It's tempting to push [skip upload] first to verify the build, then push an empty commit to actually upload. That doubles your runner minutes for no gain — failed builds skip altool anyway. Push the real commit once and read the upload log if you need to verify.
Build numbers and CURRENT_PROJECT_VERSION
Mainline expects each new TestFlight build to have a higher CFBundleVersion than the previous one. The simplest way to achieve this is to pass CURRENT_PROJECT_VERSION=$GITHUB_RUN_NUMBER to xcodebuild archive — GitHub increments the run number on every workflow run, including reruns and empty-commit triggers, so build numbers stay monotonic without you tracking them.
If you set the build number elsewhere (a Configuration file, a script, etc.), make sure that source still produces a strictly increasing value across reruns.
Single-source app icons
If you supply a single 1024×1024 master icon in an AppIcon.appiconset (the modern “Single-Size App Icons” feature, supported in Xcode 14+), three things must be in place or App Store Connect rejects the upload:
- The asset catalog has
idiom: universal, platform: iosand one 1024×1024 PNG. - The build setting
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconis set on the target. - Info.plist contains
CFBundleIconName = AppIcon.
If you generate your project with XcodeGen, put the directory containing your .xcassets under the target’s sources: list, not resources: — the latter creates a folder reference that bypasses actool and produces a bundle with no compiled icons of any size.
What Mainline doesn’t touch
Mainline doesn’t write, edit, or generate workflow YAML. The pipeline is yours. If something breaks at the YAML level — a typo in on:, an env var name change, a new Xcode action that needs configuring — that’s a manual fix in the file.
The narrow exception is Secret Injection, which writes secret values to GitHub Actions but never modifies the workflow file’s references to them.