README
¶
Flightline
App Store as Code.
The first declarative tool for App Store Connect: fetch live state, edit YAML, lint, plan, apply.
Like Terraform for cloud infrastructure or Pulumi for Kubernetes, Flightline manages your App Store presence as declarative state. A single Go binary that fetches live App Store Connect state into YAML, runs preflight checks against Apple's rejection rules, and applies changes idempotently. The same tool reads sales, analytics, reviews, subscription state, beta feedback, and performance metrics from the terminal. The ASC web UI becomes optional.
Open source under MIT, and contributions are welcome (see CONTRIBUTING.md). No SaaS layer, no telemetry, no accounts. Just a binary that talks to Apple's API.
Table of Contents
- Why Flightline
- Position
- What it does today
- Architecture
- The lifecycle
- Install
- Setup
- Quickstart
- Commands by category
- Output
- Configuration precedence
- What it doesn't do
- Documentation
- Building from source
- Development
- Status
- License
Why Flightline
Every other modern platform has "as Code" tooling: Terraform for cloud, Pulumi for Kubernetes, Crossplane for control planes, Helm for releases. The App Store doesn't. Until now.
Apple's App Store Connect has two failure modes that cost real time.
Authoring failures. Hundreds of fields scattered across a dozen surfaces: version metadata, IAPs, IAP review screenshots, age rating, export compliance, content rights, privacy nutrition labels, review notes, contact info, demo credentials, screenshot dimensions per device, per-locale localizations, build attachment, review submission item composition. Forget any one and the release gets bounced. Every rejection is a lost release cycle.
Observation friction. Sales, downloads, conversion, reviews, subscription churn, beta crashes, performance metrics, each on a different ASC web surface, none piped, none scriptable, none LLM-readable. You spend hours per week clicking through screens to answer "how is my app doing."
Flightline addresses both. The authoring half lets you declare release state in YAML next to your app source, diff it against live ASC state, and apply changes idempotently. The observation half gives you composable terminal commands you can pipe to jq, feed to LLM prompts, or cron-schedule as snapshots. Neither half is bonus. Both are first-class.
Position
Flightline slots into the established "as Code" lineage. Same shape: declarative state, idempotent reconciliation, drift detection, version control as the source of truth.
| Tool | Domain | "as Code" for |
|---|---|---|
| Terraform | AWS, GCP, Azure, on-prem | Infrastructure |
| Pulumi | Cloud + Kubernetes (general-purpose languages) | Infrastructure |
| Crossplane | Multi-cloud control planes | Resources |
| Helm | Kubernetes | Releases |
| Flightline | App Store Connect | App Store |
If you've used any of those, the workflow rhymes: fetch to capture live state, plan to diff, apply to converge. The substrate is different (Apple's API instead of a cloud), and the failure mode being prevented is App Store rejection rather than a bad cloud rollout, but the discipline is the same.
What it does today (v0.0.1-pre)
All three layers are complete: L1 (API CLI), L2 (state-as-code), and L3 (preflight rules). The columns below split the management surface (L1 read/write CLI verbs) from the as-Code surface (L2 fetch/plan/apply) so you can see what's drivable from a state.yaml versus what's CLI-only.
| Surface | L1 read | L1 write | L2 state-as-code | L3 preflight rule |
|---|---|---|---|---|
| Apps | ✅ | - | - | - |
| Versions | ✅ | ✅ | ✅ | ✅ |
| Builds (incl. attach) | ✅ | ✅ | ✅ | ✅ |
| Metadata + localizations | ✅ | ✅ | ✅ | ✅ |
| Screenshots | ✅ | ✅ | ✅ ¹ | ✅ |
| IAPs (incl. review screenshot) | ✅ | ✅ | ✅ ¹ | ✅ (3 rules) |
| Age rating | ✅ | ✅ | ✅ | ✅ |
| Export compliance | ✅ | ✅ | ✅ | ✅ |
| Reviewer demo info | ✅ | ✅ | ✅ | - |
| Categories | ✅ | ✅ | ✅ | - |
| Pricing | ✅ | ✅ | ✅ | - |
| Custom product pages | ✅ | ✅ | ✅ ¹ | - |
| TestFlight (groups, testers, beta-review submit) | ✅ | ✅ | ✅ (partial) | ✅ |
| Subscription groups | ✅ | - ² | - ² | - |
| Review submissions (App Store Review) | ✅ | - ³ | - ³ | - |
| Customer reviews | ✅ | - ⁴ | - | - |
| Beta feedback (crash + screenshot) | ✅ | - | - | - |
| Diagnostic signatures | ✅ | - | - | - |
| Performance metrics | ✅ | - | - | - |
| Sales reports | ✅ | - | - | - |
| Finance reports | ✅ | - | - | - |
| Subscription reports | ✅ | - | - | - |
| Analytics reports | ✅ | - | - | - |
| Privacy nutrition labels | portal-only ⁵ | - | - | - |
¹ Asset uploads (screenshots, IAP review screenshots, CPP screenshots) flow through L1 verbs by design: flightline screenshots upload, flightline iap review-screenshot upload, flightline custom-product-pages screenshots upload. Apple's multipart upload API (reserve → PUT → commit, often via a separate signed-URL host) is structurally distinct from JSON PATCH on a config field, so apply deliberately doesn't drive uploads. Config fields converge through apply, asset bytes flow through the upload verbs. The two-command flow (upload, then apply) is the intended workflow.
² Subscriptions are read-only for now (flightline subscriptions list/get/reports). Subscription writes (groups, products, prices, intro offers, promotional offers) are deferred, with no near-term plan.
³ App Store Review submission (the actual "ship this version" verb) is intentionally manual. flightline review-submissions list and flightline review-submissions items show submission state read-only; the create-and-submit flow happens in the ASC web portal. Rationale: review submission is a high-stakes, non-reversible action where double-checking in the portal is the safer default while the rest of the toolchain matures.
⁴ flightline reviews list/get/summary is read-only. Replying to reviews is not implemented.
⁵ appPrivacyDetails is absent from ASC API v4.3. flightline privacy-labels get returns a typed supported: false diagnostic explaining the gap rather than silently failing. Flightline will wire this when Apple adds the endpoint to the spec.
Architecture
Flightline is a cobra subcommand tree backed by a hand-rolled HTTP+JSON client against Apple's API. There is no codegen: Apple's OpenAPI spec triggers cascading type-name collisions in every Go generator evaluated. The spec is committed as authoritative reference and queried via jq during development.
flowchart TB
User["You\n(or LLM / cron)"]
YAML["state.yaml"]
CLI["flightline CLI\nmain.go"]
Lint["internal/lint\n11 preflight rules"]
Plan["internal/plan\ndiff engine"]
State["internal/state\nfetch / apply"]
ASC["internal/asc\nhand-rolled HTTP+JSON client"]
Auth["internal/auth\nES256 JWT (IEEE P1363)"]
Apple[("Apple ASC API\napi.appstoreconnect.apple.com")]
User -->|"edit"| YAML
User -->|"flightline lint / preflight"| CLI
User -->|"flightline plan / apply"| CLI
User -->|"flightline sales / reviews / ..."| CLI
YAML --> CLI
CLI --> Lint
CLI --> Plan
CLI --> State
Lint --> State
Plan --> State
State --> ASC
ASC --> Auth
Auth -->|"ES256 JWT"| Apple
ASC -->|"HTTPS"| Apple
Layer stack:
L3: preflight rules (internal/lint/) ─── catches clerical rejection causes
L2: state-as-code (internal/state/) ─── declare → diff → apply
L1: API CLI (internal/asc/) ─── every ASC surface as a terminal command
Each layer is useful standalone. You can use flightline sales and flightline reviews without ever touching a state.yaml. You can use L2 without running preflight. L3 preflight can catch issues even if you manage writes manually.
The lifecycle
Authoring (stop getting rejected)
flowchart LR
A["1. fetch\nread live state"] --> B["2. edit\nstate.yaml"]
B --> C["3. lint\noffline check"]
C --> D["4. plan\ndiff vs live"]
D --> E["5. preflight\nlive rule check"]
E --> F["6. apply --confirm\nidempotent writes"]
F --> G["7. submit\ntestflight beta-review submit"]
G --> H["8. rejection\ndiagnose if bounced"]
classDef readonly fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
classDef write fill:#fff8e1,stroke:#f9a825,color:#3e2723
classDef commit fill:#fce4ec,stroke:#c62828,color:#b71c1c
class A,C,D,E,H readonly
class B write
class F,G commit
Steps 1 to 5 are read-only and reversible. Step 6 patches ASC but does not submit for review. Step 7 (the beta-review submit) is the only action that triggers Apple Review, and it requires explicit confirmation.
Observation (stop opening the web UI)
flightline sales com.under5.passdmv --days 30
flightline finance com.under5.passdmv --month 2026-04
flightline subscriptions list com.under5.passdmv
flightline reviews list com.under5.passdmv --rating 1 --rating 2 --rating 3
flightline reviews summary com.under5.passdmv
flightline analytics request com.under5.passdmv --wait
flightline beta-feedback crash com.under5.passdmv
flightline diagnostics list com.under5.passdmv
flightline performance app com.under5.passdmv
All observation commands support --output json for piping to jq or feeding to LLM prompts.
Install
go install github.com/ul0gic/flightline@latest
Requires Go 1.26+. App Store Connect work requires a Mac, so that's where Flightline is meant to run (Apple Silicon and Intel both supported); the binary itself builds anywhere Go does.
Verify the install:
flightline --version
flightline --help
go install is the supported install method for now. To compile from a checkout, see Building from source. A Homebrew tap may follow once the project is released.
Setup
Flightline authenticates with an App Store Connect API key (a .p8 private key it signs an ES256 JWT with), not your Apple ID. One-time setup:
-
Generate a key. App Store Connect > Users and Access > Integrations > App Store Connect API > +. Grant role App Manager (or Admin for finance reports). Click Generate, then Download API Key: the
.p8downloads only once. Note the Key ID and Issuer ID. -
Place the key. Move it to
~/.appstoreconnect/AuthKey_<KEY_ID>.p8andchmod 600it. Flightline refuses a.p8with permissions wider than600and prints the exact fix. -
Export credentials. In
~/.zshrc(or~/.bashrc):export APP_STORE_CONNECT_KEY_ID="ABCD1234EF" export APP_STORE_CONNECT_ISSUER_ID="12345678-90ab-cdef-1234-567890abcdef" export APP_STORE_CONNECT_VENDOR_NUMBER="12345678" # sales/finance only -
Verify. Run
flightline whoami;AUTHORIZED truemeans you are set.
For the full walkthrough (roles, troubleshooting 401/403/chmod, alternative config paths), see docs/getting-started/apple-api-key.md.
Alternative configuration paths (lower precedence than env vars): CLI flags (--key-id, --issuer-id) on every command, or a config file at ~/.config/flightline/config.yaml. See Configuration precedence for the resolution order.
Quickstart
Five commands that verify the install works and cover both pillars:
# Verify auth
flightline whoami
# List your apps
flightline apps list
# Inspect a version
flightline versions get com.under5.passdmv --version 1.0
# Diagnose a rejection (if the version is in REJECTED state)
flightline rejection com.under5.passdmv --version 1.0
# Run offline preflight against a state file
flightline lint state.yaml
Replace com.under5.passdmv with your bundle ID. For the full state-as-code walkthrough (fetch → edit → plan → apply), see docs/guides/state-as-code.md.
Commands by category
Authoring: manage release state
# Versions
flightline versions list com.under5.passdmv
flightline versions get com.under5.passdmv --version 1.1
flightline versions create com.under5.passdmv --version 1.1 --copyright "2026 ..."
flightline versions update com.under5.passdmv --version 1.1 --release-type MANUAL
# Metadata and localizations
flightline metadata set com.under5.passdmv --version 1.1 \
--locale en-US --name "PassDMV" --subtitle "..."
# Screenshots
flightline screenshots upload com.under5.passdmv --version 1.1 \
--locale en-US --device-set APP_IPHONE_67 ./screenshots/iphone.png
# IAPs
flightline iap list com.under5.passdmv
flightline iap create com.under5.passdmv --name "Lifetime" \
--product-id com.under5.passdmv.lifetime --type NON_CONSUMABLE
# Age rating and compliance
flightline age-rating set com.under5.passdmv --version 1.1 --from-file rating.json
flightline export-compliance set com.under5.passdmv --version 1.1 \
--uses-non-exempt-encryption false
# Review submissions
flightline review-submissions list com.under5.passdmv
flightline review-submissions items com.under5.passdmv --submission <id>
# Diagnose a rejection
flightline rejection com.under5.passdmv --version 1.1
Observation: read account state
# Customer reviews
flightline reviews list com.under5.passdmv --rating 1 --rating 2
flightline reviews summary com.under5.passdmv
# Sales and finance reports
flightline sales com.under5.passdmv --days 30
flightline sales com.under5.passdmv --month 2026-04 --output json
flightline finance com.under5.passdmv --month 2026-04
# Subscription reports
flightline subscriptions list com.under5.passdmv
flightline subscriptions reports com.under5.passdmv --type summary --range P30D
# Analytics (async: request, poll, download)
flightline analytics request com.under5.passdmv --access-type ONE_TIME_SNAPSHOT --wait
flightline analytics list-instances com.under5.passdmv --report-id <id>
flightline analytics download com.under5.passdmv --instance <id> --out report.csv
# TestFlight feedback and crash diagnostics
flightline beta-feedback crash com.under5.passdmv
flightline beta-feedback screenshot com.under5.passdmv
flightline diagnostics list com.under5.passdmv
flightline diagnostics get com.under5.passdmv --signature <id>
# Performance metrics
flightline performance app com.under5.passdmv
flightline performance build com.under5.passdmv --build <id>
State as Code: declare, diff, apply
# Snapshot live ASC state into a YAML file
flightline fetch com.under5.passdmv > state.yaml
# Preview what would change (no writes)
flightline plan state.yaml
# Apply changes idempotently (safe to re-run)
flightline apply state.yaml --confirm
# Resume a partially-applied run after interruption
flightline apply state.yaml --confirm --resume
See docs/reference/state-yaml.md for the full v1alpha1 schema reference and docs/guides/state-as-code.md for a step-by-step walkthrough.
Preflight: catch rejections before they happen
# Offline: validates state.yaml against JSON Schema + format rules
flightline lint state.yaml
# Live: reads ASC state, runs all 11 rules, reports pass/fail
flightline preflight com.under5.passdmv --version 1.1
# Cross-check live state against a state file
flightline preflight com.under5.passdmv --version 1.1 --state-file state.yaml
# JSON output for CI integration
flightline preflight com.under5.passdmv --version 1.1 --output json | jq '.diagnostics'
See docs/reference/preflight-rules.md for every rule with mode, severity, fix hints, and examples.
Output
Every command supports --output table (default) and --output json.
flightline apps list --output table
BUNDLE ID NAME STATUS
com.under5.passdmv PassDMV READY_FOR_SALE
flightline apps list --output json
[
{
"bundleId": "com.under5.passdmv",
"name": "PassDMV",
"sku": "passdmv",
"primaryLocale": "en-US"
}
]
The JSON shape is a stable contract. Adding fields is backward-compatible; removing or renaming fields is a breaking change tracked by a major version bump. Sales and subscription commands additionally support --output tsv (passthrough from Apple's wire format).
Configuration precedence
From highest to lowest priority:
- CLI flags (
--key-id,--issuer-id, etc.) - Environment variables (
APP_STORE_CONNECT_KEY_ID,APP_STORE_CONNECT_ISSUER_ID,APP_STORE_CONNECT_VENDOR_NUMBER,APP_STORE_CONNECT_KEY_PATH,FLIGHTLINE_*) - Config file (
~/.config/flightline/config.yaml) - Defaults
Config file example (~/.config/flightline/config.yaml):
key-id: ABCD1234EF
issuer-id: 12345678-90ab-cdef-1234-567890abcdef
vendor-number: "12345678"
output: table
What it doesn't do
Not Fastlane. No pipeline DSL, no build orchestration. Flightline is the ASC config and reporting layer. xcodebuild, Xcode Cloud, and Fastlane still own compilation, signing, and binary upload.
Not a build tool. Flightline doesn't compile, archive, or upload .ipa files. You point a build at a version with builds attach; Flightline handles everything from that point forward.
Not a screenshot generator. Flightline uploads screenshots you provide via screenshots upload.
Not a SaaS. No backend, no telemetry, no accounts. The binary talks directly to Apple's API using your credentials.
Not the App Store Review submit button, by design. Flightline preps everything that goes into a submission (build attach, metadata, screenshots, IAPs, age rating, export compliance, demo creds), and flightline preflight will tell you whether the version is submission-ready, but the final "Submit for Review" click happens in the ASC web portal. Review submission is high-stakes and non-reversible; keeping the human-in-the-loop step in the portal where you can also see the full submission summary is the safer default for now. May be wired as flightline review-submissions submit in a later version once the rest of the toolchain has more real-world miles on it.
Three portal-only surfaces. Apple's public API does not expose these. Flightline tells you explicitly when you hit them rather than silently failing:
- Resolution-center reviewer messages: the rejection text written by Apple's reviewers is not in the v4.3 API.
flightline rejectionreports every API-visible state field and tells you to check the portal for the actual message. - Privacy nutrition labels (
appPrivacyDetails): entirely absent from ASC API v4.3.flightline privacy-labels getreturns a typedsupported: falsediagnostic. Flightline will wire this when Apple adds the endpoint to the spec. - App Store Review submission (covered above): wired as a deliberate human-in-the-loop step, not an API gap.
Documentation
Getting started
| Document | What it covers |
|---|---|
| docs/getting-started/install.md | Install via go install or from source |
| docs/getting-started/apple-api-key.md | Full App Store Connect API key setup: generate, place the .p8, export env vars, verify |
| docs/getting-started/first-run.md | The first five read-only commands |
Guides
| Document | What it covers |
|---|---|
| docs/guides/state-as-code.md | Fetch → edit → plan → apply walkthrough using passdmv |
| docs/guides/uploading-assets.md | Uploading screenshots and IAP review screenshots via the L1 verbs |
Reference
| Document | What it covers |
|---|---|
| docs/reference/state-yaml.md | Full v1alpha1 reference: every field, type, constraint, and gotcha |
| docs/reference/preflight-rules.md | All 11 preflight rules + submission-checklist items Apple's API doesn't expose |
| docs/reference/cli.md | Command-group index pointing at flightline <group> --help |
Concepts
| Document | What it covers |
|---|---|
| docs/concepts/three-layer-model.md | How L1 (API CLI), L2 (state-as-code), and L3 (preflight) fit together |
Building from source
Requires Go 1.26 or later.
Clone and build
git clone https://github.com/ul0gic/flightline.git
cd flightline
make build
./bin/flightline --version
The binary is at ./bin/flightline. Move it onto your PATH if you want it system-wide:
sudo mv ./bin/flightline /usr/local/bin/flightline
Or via go install
go install github.com/ul0gic/flightline@latest
The binary lands in $GOBIN (typically ~/go/bin). Make sure ~/go/bin is on your PATH.
Development
make build # produces ./bin/flightline
make test # go test ./... -race
make vet # go vet ./...
make lint # golangci-lint run
make verify # vet + test + lint (the gate)
make fmt # gofmt -s -w . && goimports -w .
make clean # remove ./bin and coverage artifacts
Architecture decisions live in .project/ (gitignored, internal scaffolding). The design rationale for the hand-rolled API client instead of codegen, the JWT IEEE P1363 requirement, the async-poll state persistence model, and every other non-obvious choice are documented there.
Adding a command: query openapi.oas.json with jq for the endpoint shape, add a file under internal/asc/ for the client function, add a file under internal/cmd/ for the cobra command, add a golden fixture under internal/asc/testdata/golden/. See existing files for the pattern. It is consistent throughout.
Tests: unit tests for all command logic, HTTP fixture replay tests for the client (captured Apple responses replayed via a local server), integration tests behind //go:build integration. Run make test before any commit.
Status
v0.0.1-pre, not yet released. L1 (full API CLI, both authoring and observation pillars), L2 (state-as-code with fetch/plan/apply), and L3 (11 preflight rules) are all complete.
Planned next: GoReleaser-built release binaries, a Homebrew tap, and retiring the legacy Node CLI.
Open-sourced under MIT and maintained by ul0gic. Contributions are welcome: read CONTRIBUTING.md first. Cadence is evenings and weekends, not a funded project, so reviews may take a few days.
License
MIT. See LICENSE.
Documentation
¶
There is no documentation for this package.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
gen-docs
command
Command gen-docs regenerates the mechanical reference docs from source so they cannot drift.
|
Command gen-docs regenerates the mechanical reference docs from source so they cannot drift. |
|
internal
|
|
|
asc
State persists to $XDG_STATE_HOME/flightline/<bundleId>/<reportClass>.json using atomic rename.
|
State persists to $XDG_STATE_HOME/flightline/<bundleId>/<reportClass>.json using atomic rename. |
|
auth
Package auth handles ASC API auth.
|
Package auth handles ASC API auth. |
|
cmd
The --password flag is a credential surface: it is never logged, never echoed by --verbose, and filtered out of every error string before return.
|
The --password flag is a credential surface: it is never logged, never echoed by --verbose, and filtered out of every error string before return. |
|
config
Package config loads and validates Flightline state YAML files.
|
Package config loads and validates Flightline state YAML files. |
|
lint
Package lint implements Flightline's preflight rule engine.
|
Package lint implements Flightline's preflight rule engine. |
|
plan
Package plan computes the leaf-level change set between desired and live *config.State.
|
Package plan computes the leaf-level change set between desired and live *config.State. |
|
state
Package state implements Fetch (live ASC → typed *State) and Apply (change set → ASC writes).
|
Package state implements Fetch (live ASC → typed *State) and Apply (change set → ASC writes). |