Flightlinev0.7.0

Docs/Guides/State as Codeview as markdown

State as Code: a 5-minute walkthrough

What you'll do

In about 5 minutes: fetch the live App Store Connect state of an existing app into a YAML file, edit one field, preview the diff, and apply it. The example uses tideterm (app.tideterm.ios) throughout, but every command takes a bundle ID, substitute your own.

When you finish, you'll have a state.yaml that is the authoritative source of truth for your app's ASC configuration, diffable in git, and applicable idempotently.


Prerequisites

Flightline installed:

go install github.com/ul0gic/flightline@latest
flightline --version

ASC API key:

  1. In App Store Connect, go to Users and Access → Integrations → App Store Connect API.
  2. Generate a key with App Manager role (or Admin for sales and finance reports). Download the .p8 file.
  3. Place it at ~/.appstoreconnect/AuthKey_<KEY_ID>.p8 with mode 600:
chmod 600 ~/.appstoreconnect/AuthKey_<KEY_ID>.p8

Environment variables:

export APP_STORE_CONNECT_KEY_ID=XXXXXXXXXX
export APP_STORE_CONNECT_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export APP_STORE_CONNECT_VENDOR_NUMBER=12345678

Add these to your shell profile or a .envrc (gitignored). Flightline reads them at startup; you can also pass --key-id and --issuer-id flags.


1. Fetch live state

Pull the current ASC state for your app into a file:

flightline fetch app.tideterm.ios -o state.yaml

Or capture to stdout and redirect:

flightline fetch app.tideterm.ios > state.yaml

Flightline writes to stderr when using -o:

flightline: wrote state.yaml (2847 bytes)

Open state.yaml. The top of the file will look like:

# yaml-language-server: $schema=https://flightline.dev/schemas/v1alpha1/state.schema.json
#
# Generated by `flightline fetch`, edit, then `flightline plan` to preview
# the diff against live ASC, and `flightline apply --confirm` to write.

apiVersion: flightline.dev/v1alpha1
kind: AppState

metadata:
  bundleId: app.tideterm.ios
  version: "1.0.1"
  platform: IOS

spec:
  version:
    releaseType: AFTER_APPROVAL
    copyright: © 2026 Tideterm Labs

  build:
    number: "42"

  metadata:
    locales:
      en-US:
        name: .PassDMV: California
        subtitle: Pass the test, first try
        description: |
          California DMV practice tests with the latest official questions.
          Track your progress, focus on weak areas, and pass with confidence.
        keywords: DMV,driver,test,license,California,permit,practice
        promotionalText: Free updates as the DMV question bank evolves.
        marketingUrl: https://tideterm.app
        supportUrl: https://tideterm.app/support
        privacyPolicyUrl: https://tideterm.app/privacy
  ...

The # yaml-language-server: $schema=... line activates autocomplete in VS Code and any editor with the YAML language server. If you type spec. on a new line, your editor will offer completions.

What got fetched. Every surface Flightline supports is represented: version, build, metadata, screenshots, IAP, age rating, export compliance, reviewer demo, categories, pricing, TestFlight groups, and custom product pages. Surfaces with no live data are omitted (nil pointers mean "not managed").


2. Edit a field

Open state.yaml and change the promotionalText for en-US:

spec:
  metadata:
    locales:
      en-US:
        promotionalText: "Now with updated 2026 question bank."

Promotional text is the only metadata field you can update on a live, approved version without a resubmission, so it's a good field to experiment with.

Save the file.


3. Plan the diff

Preview what Flightline would change, without writing anything:

flightline plan state.yaml

Expected output:

OP     PATH                                         FROM                                       TO
~      /spec/metadata/locales/en-US/promotionalText Free updates as the DMV question bank...   Now with updated 2026 question bank.

~ means update. + means create. - means delete. Read-only: flightline plan never writes.

JSON output for scripts:

flightline plan state.yaml --output json
{
  "bundleId": "app.tideterm.ios",
  "version": "1.0.1",
  "changes": [
    {
      "op": "~",
      "path": "/spec/metadata/locales/en-US/promotionalText",
      "from": "Free updates as the DMV question bank evolves.",
      "to": "Now with updated 2026 question bank."
    }
  ]
}

CI use. If you want a non-zero exit code when there are changes (useful in a CI gate):

flightline plan state.yaml --exit-on-changes
# exits 0 when no changes, 2 when changes exist, 1 on error

4. Apply

Write the changes to ASC:

flightline apply state.yaml --confirm

Without --confirm, flightline apply prints the plan and refuses to write, the safety gate. This matches Terraform's behavior: apply without --confirm is equivalent to plan.

Expected stderr output during apply:

flightline: applied ~ /spec/metadata/locales/en-US/promotionalText

The command exits 0 on success. If any individual change fails, Flightline continues with the remaining changes, surfaces the errors in the output, and checkpoints completed changes for a safe resume.

Checkpoint and resume. If an apply is interrupted (Ctrl-C, network drop, crash), Flightline stores a coordinate-scoped checkpoint in the user cache. It binds the checkpoint to the bundle ID, version, platform, and original plan. Resume with:

flightline apply state.yaml --confirm --resume

Flightline fetches live state again, verifies that the remaining diff matches the interrupted plan, and applies that residual diff. A mismatch stops before any write and asks you to start a fresh apply. A fully successful apply removes its checkpoint.

Dry run. Fetch live state and compute the full dispatch path without sending mutating requests:

flightline apply state.yaml --dry-run --output json

Dry-run still requires credentials and network access because it reads current ASC state. It sends no POST, PATCH, or DELETE requests.


5. Re-plan to confirm idempotency

Run plan again:

flightline plan state.yaml

Expected output:

OP      PATH    FROM    TO
(none)  no changes

Flightline fetched the live state, compared it to your file, and found no differences. The round-trip is clean. This is the contract: applying the same state file twice produces no second write.


When you'd use which command

Command What it does Writes?
flightline fetch <bundleId> Snapshot live ASC state into YAML No
flightline fetch <bundleId> --version 1.0 Fetch a specific version No
flightline fetch <bundleId> --output json Fetch as JSON (for piping, not editing) No
flightline plan state.yaml Preview what would change No
flightline plan state.yaml --exit-on-changes Plan + CI gate exit code No
flightline plan state.yaml --output json Machine-readable diff No
flightline apply state.yaml Plan only, refuses to write No
flightline apply state.yaml --confirm Write all changes Yes
flightline apply state.yaml --confirm --resume Resume after interruption Yes
flightline apply state.yaml --dry-run Read live state and compute dispatch, no mutations No

Typical workflow

1. Edit       edit state.yaml
2. Lint       flightline lint state.yaml           # offline schema + format rules
3. Plan       flightline plan state.yaml           # read-only diff, no writes
4. Apply      flightline apply state.yaml --confirm
5. Preflight  flightline preflight <bundleId> -v <v>  # live rule check
6a. External TestFlight  flightline testflight beta-review submit <bundleId> --build <n>
6b. App Store release   attach the build and intended IAPs, then Submit for Review in ASC

Steps 1 to 5 are reversible. The two terminal paths are separate: beta review unlocks external TestFlight distribution and does not submit a production release. App Store Review submission remains a manual ASC action; run preflight immediately before it and verify the review submission contains the build and every intended IAP.


Trial-gated paywalls: tell the reviewer where the purchase lives

ASC state cannot see in-app gating. If your paywall only appears after a free trial expires, the reviewer's fresh install will never show it — and "we cannot locate the In-App Purchases" rejections follow. Two rules of thumb:

  • Your App Review notes MUST give the exact steps to reach the purchase (flightline reviewer-demo manages this surface; preflight's review-details.completeness rule enforces it once an IAP is in the submission).
  • Expose a purchase entry point that is reachable at all times — a buy button in Settings during the trial costs nothing and removes the whole rejection class.

Current limitations

Screenshots and IAP review screenshots

flightline apply drives binary asset uploads for version screenshots, IAP review screenshots, and Custom Product Page screenshots. Asset paths are resolved relative to the state file. The diff engine compares local MD5 values with Apple's live checksums, so unchanged bytes are skipped:

flightline plan state.yaml
flightline apply state.yaml --confirm

For managed screenshot sets, files absent from the desired list are deleted during the confirmed apply. --resume also resumes interrupted multipart uploads after validating their file checksums. See Uploading assets for the full workflow and the optional one-off upload commands.

Privacy nutrition labels

Privacy nutrition labels are not manageable via the App Store Connect API (v4.3). The API has no endpoint for appPrivacyDetails. Manage them in the App Store Connect web UI.

flightline privacy-labels get <bundleId> returns a diagnostic explaining this gap; it does not fail silently.


Troubleshooting

asc: HTTP 401 Unauthorized

Your credentials are wrong or expired. Check:

# Confirm the env vars are set and non-empty
echo $APP_STORE_CONNECT_KEY_ID
echo $APP_STORE_CONNECT_ISSUER_ID

# Confirm the .p8 exists at the expected path
ls ~/.appstoreconnect/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8

JWTs are minted per-request and expire after 20 minutes, there's no token cache to invalidate.

asc: HTTP 403 Forbidden

Your key exists but lacks permission for this operation. Check the role on your API key in App Store Connect (Users and Access → Integrations). App Manager or Admin roles are required for most write operations.

config: chmod 600 ~/.appstoreconnect/AuthKey_<id>.p8

Your .p8 file mode is too wide (world-readable or group-readable). Fix it:

chmod 600 ~/.appstoreconnect/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8

Flightline refuses to load a key file with permissions wider than 600.

Strict YAML errors (yaml: line N: cannot unmarshal !!int into string)

Flightline's strict loader rejects YAML type coercions. Common fixes:

# Wrong, YAML parses these as non-strings
version: 1.0
number: 42
appPricePointId: FREE

# Right, quote them
version: "1.0"
number: "42"
appPricePointId: "FREE"

plan: state.yaml failed schema validation

The file has a field-level error. The diagnostic output includes file:line:col for YAML errors and a JSON Pointer path for schema errors. Fix the specific field and re-run.

asc: no version "1.0.2" on platform IOS

The version in metadata.version doesn't exist in ASC yet. Either create the version first (flightline versions create) or update metadata.version to match an existing editable version.

state: ageRating.seventeenPlus is derived by Apple; cannot be set directly

The seventeenPlus field in spec.ageRating is Apple-computed from your questionnaire answers. Remove it from the spec or leave it as read-only documentation. It has no write effect.


See also

  • Field reference, every field, every constraint, every gotcha
  • Uploading assets, the screenshot and IAP review-screenshot upload workflow
  • Preflight in CI, run live release checks as a deployment gate
  • Schema: schemas/flightline.schema.json (or https://flightline.dev/schemas/v1alpha1/state.schema.json)
  • flightline --help, flightline fetch --help, flightline plan --help, flightline apply --help