Flightlinev0.8.0

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:

Shell
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:

Shell
# 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/null

Related 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):

JSON
{
  "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:

Shell
# 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).

YAML
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.yaml

Notes:

  • 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 600 is 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).
  • preflight defaults --platform to IOS; pass --platform MAC_OS (or TV_OS, VISION_OS) for other platforms.
  • Pin a verified release tag or source revision instead of tracking @latest in 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:

Shell
flightline preflight com.example.app --version 2.1.0 --state-file state.yaml

This 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