Flightlinev0.7.0

Docs/Reference/Preflight rulesview as markdown

Preflight rules

Flightline's L3 layer catches the clerical mistakes that cause Apple to reject a release. Each rule encodes a real rejection pattern. The ruleId is a stable JSON contract; renaming one is a breaking change.

Two commands run the rules: flightline lint <state.yaml> (offline) and flightline preflight <bundleId> --version <v> (live). Both emit a stable JSON contract via --output json.

Severity and exit codes

Severity Meaning Exit code
error Known rejection cause or structural problem that blocks apply. Preflight exits non-zero. 1
warning Worth fixing; Apple is unlikely to hard-reject for it. 2 (when no errors present)
info A reminder or hint. Never gates submission. 0

When both errors and warnings are present, exit code is 1.

Quick index

Rule ID Mode Severity
agreements.paid-apps-unverifiable Live Info
build.attached-and-valid Live Error
iap.attached-to-review-submission Live Error
iap.localization-name-is-language-name Offline Warning
iap.promotional-image-distinct Live Error
iap.review-screenshot-exists Live Error
iap.state-submittable Live Error
localizations.completeness Offline Warning
review-details.completeness Live Warning
screenshots.required-devices Both Error
strict.format-email Offline Warning
strict.required-nonzero Offline Error
strict.yaml-coercion Offline Error
version.age-rating-answered Both Error
version.export-compliance-answered Both Error

Rules are sorted alphabetically by ID, the same order the runner executes them in.

Rule reference

agreements.paid-apps-unverifiable

  • Mode: Live
  • Severity: Info

Reminds you to confirm the Paid Apps Agreement whenever the app sells in-app purchases. Apple exposes no API for agreement status, and a lapsed or re-issued agreement makes every IAP silently invisible to the reviewer's sandbox — the rejection reads "IAP product not found" with nothing wrong in ASC. Confirm the agreement is Active under Business in App Store Connect before submitting; this diagnostic is informational and never fails preflight.

build.attached-and-valid

  • Mode: Live
  • Severity: Error

Checks that the App Store version has a build attached and that the build's processingState is VALID. Apple's submission flow hard-blocks Submit for Review until both hold, but the block only surfaces at submit time after you have confirmed the dialog, so the gap is easy to miss until the worst moment. Fix it by uploading the build, waiting for processing to reach VALID, then attaching it to the version.

iap.attached-to-review-submission

  • Mode: Live
  • Severity: Error

Checks that every in-app purchase marked READY_TO_SUBMIT also appears in the app's open review submission. This is the most common IAP rejection cause: developers mark an IAP ready and assume that means submitted, so the app goes through review without it and the IAP never goes live. Fix it by attaching the IAP to the review submission, or by creating a submission first if none is open.

iap.localization-name-is-language-name

  • Mode: Offline
  • Severity: Warning

Warns when an in-app purchase localization's display name is a language name like "English" or "Español" — the classic paste bug where the locale label lands in the product-name field. Customers see this name on the purchase sheet, and Apple's reviewer will describe the IAP by it, turning the mistake into a confusing rejection. Fix it by naming what the customer buys ("Lifetime Access", "Acceso de por Vida"), never the language it is written in.

iap.promotional-image-distinct

  • Mode: Live
  • Severity: Error

Checks that an IAP review screenshot does not reuse one of the app's store screenshots, comparing source file checksums. Apple Guideline 2.3.2 hard-rejects reusing a store screenshot as IAP promotional art, and developers who drag the same image into both fields to save time hit this. Fix it by supplying a distinct image that specifically represents the IAP purchase rather than the app in general.

iap.review-screenshot-exists

  • Mode: Live
  • Severity: Error

Checks that every IAP in a state where Apple will imminently review it has an App Store review screenshot attached. The screenshot field is buried several tabs deep in App Store Connect and is easy to skip, but Apple requires it and returns the submission immediately when it is missing. Fix it by uploading a review screenshot that shows the IAP purchase within the app context.

iap.state-submittable

  • Mode: Live
  • Severity: Error

Errors when any in-app purchase is in DEVELOPER_ACTION_NEEDED, MISSING_METADATA, or REJECTED state at preflight time. Resubmitting the app without fixing these guarantees another rejection loop: the reviewer cannot find or approve a product Apple has flagged back to you. Fix it by resolving the flagged action or completing the metadata in App Store Connect, then re-running preflight.

localizations.completeness

  • Mode: Offline
  • Severity: Warning

Checks that each managed metadata locale has Flightline's submission baseline fields (name, description, and supportUrl), and that every locale appearing in one localizable surface (metadata, screenshots, or IAP localizations) also appears in the others. Apple may reject a listing where a locale has metadata but no screenshots because reviewers cannot preview it in that language, and the gap is often the sign of a half-applied edit. Fix field gaps at the diagnostic path, then add the locale to every intended surface or remove it where it should not be managed.

review-details.completeness

  • Mode: Live
  • Severity: Warning

Warns when an app that sells in-app purchases submits a version whose App Review notes are empty, and escalates to an error once an IAP is attached to the review submission. Reviewers who cannot see the purchase flow respond with an Information Needed rejection asking for it — and for demo credentials the app may not even have; trial-gated paywalls are the top reason reviewers cannot find an IAP. Fix it by writing notes that give the exact steps to reach the purchase, describe any trial mechanics, and state explicitly when the app has no accounts or sign-in.

screenshots.required-devices

  • Mode: Both
  • Severity: Error

Checks that every locale has at least one screenshot set from the large-iPhone tier Apple accepts for submission: 6.9 inch, 6.7 inch, or 6.5 inch. Apple requires the 6.9-inch size unless a 6.5-inch set is provided, and scales the largest set you supply down to smaller displays — so any one tier member unblocks Submit for Review. Fix it by uploading screenshots for one of the accepted device classes in each affected locale; 6.9 inch gives the best scaled quality.

strict.format-email

  • Mode: Offline
  • Severity: Warning

Checks that the reviewer-demo contact email and TestFlight tester emails look like real addresses. The JSON Schema declares these fields as format email, but the validator does not enforce format keywords, so a value like joe at example dot com passes schema validation silently and only fails later at Apple's wire layer. Fix it by writing each address in standard local@domain.tld form.

strict.required-nonzero

  • Mode: Offline
  • Severity: Error

Checks that schema-required string fields, notably each TestFlight tester email, are present and non-empty in the authored YAML. Go marshals a missing string field as an empty string, which satisfies the schema's required constraint (the key is present) even though the value is blank, so the row is silently dropped at apply time because there is no address to invite. Fix it by giving every required field a real non-empty value.

strict.yaml-coercion

  • Mode: Offline
  • Severity: Error

Checks that boolean fields use true or false rather than the YAML 1.1 tokens yes, no, on, or off. The YAML library implements the 1.1 core schema and coerces those tokens to booleans even when they are quoted, so gambling: "yes" decodes to true and is applied to App Store Connect without warning. Fix it by replacing every such token with a real true or false.

version.age-rating-answered

  • Mode: Both
  • Severity: Error

Checks that every prompt in the age-rating questionnaire has a value, across both the frequency-enum fields (violence, sexual content, profanity, and so on) and the boolean prompts (gambling, unrestricted web access). A partially answered questionnaire shows up only as a soft block on the Submit for Review button, and Apple will not tell you which field is missing until you open the specific panel. Fix it by giving every prompt a value; NONE for frequency fields and false for boolean prompts are valid answers meaning the content is absent. Derived and optional fields (seventeenPlus, kidsAgeBand) are exempt: Apple computes seventeenPlus itself and kidsAgeBand only applies to Kids-category apps.

version.export-compliance-answered

  • Mode: Both
  • Severity: Error

Checks that the export-compliance encryption question (usesNonExemptEncryption) has an answer on record. Apple blocks submission until this declaration is set, either per-version in App Store Connect or via ITSAppUsesNonExemptEncryption in the build's Info.plist, and forgetting it is one of the most common blocks for a new build. Fix it by setting usesNonExemptEncryption to false (correct for most apps, since standard HTTPS is exempt) or to true with the supplemental declaration.