Docs/Guides/Preflight in CIView Markdown
On this page
Preflight in CI
Run lint and preflight in CI to check state files and live release configuration. Use their exit codes to decide whether a pipeline should continue. The preflight rules reference describes each check.
lint vs preflight
Two commands run the rules, at different points in the pipeline:
| Command | Needs credentials? | Input | Run it |
|---|---|---|---|
flightline lint state.yaml |
No | Your YAML, offline | On every push and pull request |
flightline preflight <bundleId> --version <v> |
Yes | Live ASC state | Before a release, on a protected branch |
lint validates the state file against the embedded JSON Schema plus every offline rule (YAML coercion traps, required-but-empty fields, email format, localization completeness, screenshot device coverage). It makes no network calls, so it belongs in the fast path of every PR: no secrets, no rate-limit budget, sub-second.
preflight fetches the live App Store version and runs the live rules on top: build attached and valid, IAPs attached to the review submission, review screenshots present, age rating and export compliance answered. It checks the supported readiness rules against current observations. Passing does not guarantee submission acceptance or Apple approval.
Exit codes
Both commands implement the same tri-state contract:
| Exit code | Meaning |
|---|---|
0 |
Clean: no diagnostics, or info-only |
1 |
At least one error-severity diagnostic |
2 |
Warnings only, no errors |
A bare invocation is already a strict CI gate: any nonzero exit fails the step, so warnings block the pipeline along with errors, and the diagnostics (with fix hints) are in the step log.
To fail on errors but let warnings through, accept exit 2 explicitly:
flightline preflight com.example.app --version 2.1.0 || [ $? -eq 2 ]The || [ $? -eq 2 ] converts a warnings-only result back into step success; exit 1 still fails.
For finer-grained gates, the JSON summary carries exact counts. jq -e exits nonzero when the expression is false, which fails the step:
# Preserve command failures, then allow at most 5 warnings and no errors.
status=0
flightline lint state.yaml --output json > lint-result.json || status=$?
case "$status" in
0|2) ;;
*) exit "$status" ;;
esac
jq -e '.summary.error == 0 and .summary.warning <= 5' lint-result.json > /dev/nullRelated gate: flightline plan state.yaml --exit-on-changes exits 2 when the file has drifted from live ASC state, 0 when in sync. Useful as a drift detector on a schedule.
JSON output for machines
--output json emits a stable envelope (see the JSON output reference):
{
"bundleId": "com.example.app",
"version": "2.1.0",
"mode": "preflight",
"diagnostics": [
{
"ruleId": "version.export-compliance-answered",
"severity": "error",
"message": "the build attached to this version has not declared usesNonExemptEncryption",
"path": "/spec/exportCompliance/usesNonExemptEncryption",
"fixHint": "answer the export-compliance question in App Store Connect, or `flightline export-compliance set <bundleId> --version <v> --uses-non-exempt-encryption=false`."
}
],
"summary": { "error": 1, "warning": 0, "info": 0 }
}Useful extractions:
# Every error, one line each, for a PR comment
flightline lint state.yaml --output json \
| jq -r '.diagnostics[] | select(.severity == "error") | "\(.ruleId): \(.message)"'
# Machine-readable pass/fail counts
flightline preflight com.example.app --version 2.1.0 --output json | jq '.summary'GitHub Actions example
Two jobs: lint runs everywhere with no secrets; preflight runs on main with the API key from repository secrets. Store the raw contents of the .p8 file as the secret ASC_PRIVATE_KEY, and the Key ID and Issuer ID as ASC_KEY_ID and ASC_ISSUER_ID (the same values as the environment variables).
name: preflight
on:
push:
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- run: go install github.com/ul0gic/flightline@latest
- run: flightline lint state.yaml
preflight:
if: github.ref == 'refs/heads/main'
needs: lint
runs-on: ubuntu-latest
env:
APP_STORE_CONNECT_KEY_ID: ${{ secrets.ASC_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- run: go install github.com/ul0gic/flightline@latest
- name: Install API key
run: |
mkdir -p ~/.appstoreconnect
printf '%s' "$ASC_PRIVATE_KEY" > ~/.appstoreconnect/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8
chmod 600 ~/.appstoreconnect/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8
env:
ASC_PRIVATE_KEY: ${{ secrets.ASC_PRIVATE_KEY }}
- run: flightline preflight com.example.app --version 2.1.0 --state-file state.yamlNotes:
- Both bare invocations are strict: warnings (exit
2) fail the step. Append|| [ $? -eq 2 ]to a run line to gate on errors only. - The
chmod 600is not optional: Flightline refuses a key file with wider permissions. - Flightline never logs the key, the JWT, or credential IDs; error output is redacted. Still, scope the key to the least role that works (App Manager).
preflightdefaults--platformtoIOS; pass--platform MAC_OS(orTV_OS,VISION_OS) for other platforms.- Pin a verified release tag or source revision instead of tracking
@latestin a production gate. These guides describe current source; check command availability in the binary you install.
Cross-checking with --state-file
Without --state-file, preflight fetches live state and checks it: pure "is ASC submittable?". With --state-file, the offline rules run against your authored YAML while the live rules consult ASC, so one command catches authoring mistakes and live gaps together:
flightline preflight com.example.app --version 2.1.0 --state-file state.yamlThis is the right form for a release pipeline that manages state as code: it verifies both that the file you are about to apply is well-formed and that the live version passes the supported readiness checks. It does not prove the desired changes have already been applied; inspect the plan separately. For the full authoring loop around it, see State as Code.
Flightline refuses to combine unrelated coordinates. The file's metadata.bundleId and metadata.version must exactly match the command, and an explicit metadata.platform must match --platform. If the file omits metadata.platform, it inherits the command platform (IOS by default). JSON output includes the resolved platform alongside bundleId and version.
See also
- Preflight rules reference, every rule with mode, severity, and fix hints
- JSON output contract, the diagnostic envelope in detail
- API key setup, roles and credential troubleshooting