npm package: @shyguy_studio/shipkit
Description: Programmatic App Store submission. One YAML, one command.
Author: Shy Guy LLC
Latest version: 0.2.5

# @shyguy_studio/shipkit

Programmatic App Store submission. One YAML config, one command to ship.

```bash
npm install -g @shyguy_studio/shipkit
shipkit init
shipkit ship
```

## What it does

ShipKit wraps Apple's App Store Connect API into a single CLI. Build, upload, push metadata, and submit (or re-submit) for review — without opening Xcode or App Store Connect. One YAML config replaces Fastlane's 5+ files.

## Install

```bash
npm install -g @shyguy_studio/shipkit

# Verify
shipkit --version  # 0.2.4
```

Or use without installing:

```bash
npx @shyguy_studio/shipkit@latest init
```

## Quick Start

```bash
# 1. Generate config (detects Xcode, XcodeGen, workspaces)
shipkit init

# 2. Edit shipkit.yml — metadata, review notes, etc.

# 3. Full pipeline: build → upload → metadata → submit
shipkit ship
```

## Commands

| Command | What it does |
|---------|------|
| `shipkit init` | Interactive setup, generates `shipkit.yml` |
| `shipkit auth` | Validate API credentials |
| `shipkit build` | Build and archive (`xcodebuild`) |
| `shipkit upload` | Upload .ipa to App Store Connect |
| `shipkit metadata push` | Push name, subtitle, description, keywords, URLs, categories, review notes |
| `shipkit metadata pull` | Pull current version-level metadata from ASC |
| `shipkit submit` | Submit (or re-submit) for App Store review |
| `shipkit submissions list` | List every reviewSubmission with state |
| `shipkit submissions cancel <id>` | Cancel a stuck non-terminal submission |
| `shipkit status` | Check review status |
| `shipkit ship` | Full pipeline: build → upload → metadata → submit |
| `shipkit telemetry status/enable/disable` | Manage anonymous usage analytics |

## Config (shipkit.yml)

```yaml
version: 1

auth:
  key_id: "${ASC_KEY_ID}"          # Env var interpolation works
  issuer_id: "${ASC_ISSUER_ID}"
  key_path: ./keys/AuthKey.p8

app:
  bundle_id: com.example.myapp
  apple_id: "6761797247"           # Optional, ASC app ID
  team_id: 5UF3Q334K6
  name: "My App"                   # Internal display only (see note below)

build:
  scheme: MyApp
  configuration: Release
  export:
    method: app-store
  version:
    marketing: "1.0.0"
    build: auto                    # Queries ASC, increments latest

metadata:
  default_locale: en-US
  locales:
    en-US:
      # App-level (pushed via appInfoLocalizations)
      name: "My App"                 # 30 char limit
      subtitle: "Short tagline"      # 30 char limit
      privacy_url: "https://..."     # Moved to app-level by Apple

      # Version-level (pushed via appStoreVersionLocalizations)
      description: |
        Multi-line description...    # 4000 char limit
      keywords: "kw1,kw2,kw3"        # 100 char limit, no spaces after commas
      promotional_text: "..."        # 170 char limit
      marketing_url: "https://..."
      support_url: "https://..."
      whats_new: "Bug fixes."        # 4000 char limit

  # Optional categories
  categories:
    primary: "REFERENCE"
    secondary: "BOOKS"

  # Optional App Review Information
  review:
    first_name: "Jane"
    last_name: "Doe"
    email: "you@example.com"
    notes: |
      Notes for the App Reviewer. Explain
      what changed, provide demo credentials, etc.

release:
  type: manual                     # manual | automatic | phased

hooks:
  pre_build:
    - "xcodegen generate"
```

## SDK

Every operation is available as a TypeScript import:

```typescript
import { ShipKit } from "@shyguy_studio/shipkit";

const kit = new ShipKit({
  keyId: process.env.ASC_KEY_ID!,
  issuerId: process.env.ASC_ISSUER_ID!,
  keyPath: "./keys/AuthKey.p8",
});

// Modules: apps, appInfo, builds, metadata, submissions
const app = await kit.apps.find("com.example.myapp");
const status = await kit.submissions.getStatus(app.id);

// Push app-level metadata (name, subtitle, privacy URL, categories)
const appInfo = await kit.appInfo.getEditable(app.id);
await kit.appInfo.pushLocale(appInfo!.id, "en-US", {
  name: "My App",
  subtitle: "Short tagline",
  privacyPolicyUrl: "https://example.com/privacy",
});

// List reviewSubmissions (for diagnosing stuck state)
const submissions = await kit.submissions.listForApp(app.id);

// Submit or resubmit — auto-cleans up stuck submissions
await kit.submissions.submit(app.id, versionId);
```

## After a Rejection

ShipKit handles the full rejection-to-resubmission loop. Typical flow:

```bash
# 1. Fix the issue (edit shipkit.yml metadata, or fix code + bump build)

# 2. Push the new metadata (version-level + app-level + review notes)
shipkit metadata push

# 3. Inspect stuck submissions (optional)
shipkit submissions list

# 4. Re-submit — auto-releases the version from the rejected/orphaned submission
shipkit submit
```

**Don't forget**: Reply in App Store Connect → Resolution Center. That messaging system is not in Apple's public API so it must be done via the web UI.

See [shyguy.studio/shipkit/docs#rejection](https://shyguy.studio/shipkit/docs#rejection) for the full playbook.

## Requirements

- **App Store Connect API Key** — must be a **Team Key**, not Individual Key. Generated at ASC → Users and Access → Integrations → Team Keys. Role must be Admin or App Manager.
- **macOS** for `build` and `upload` commands (metadata, submit, status, submissions work anywhere)
- **Node.js 18+**
- **Xcode** if you're using `shipkit build`

## Gotchas

These are the non-obvious things we learned building and using ShipKit. Most are documented in detail at [shyguy.studio/shipkit/docs#troubleshooting](https://shyguy.studio/shipkit/docs#troubleshooting).

- **Team Keys only.** Individual keys return 401 on the ASC REST API. No workaround — regenerate as Team Key.
- **`privacyPolicyUrl` is app-level, not version-level.** Apple moved it in 2024. ShipKit v0.2.1+ handles this — leave the field where it is in your YAML.
- **App name lives on `appInfoLocalizations.name`**, NOT on the `apps` resource. Setting `app.name` in YAML only affects internal ShipKit logging. Use `metadata.locales.<locale>.name` to change the App Store display name.
- **`POST /appStoreVersionSubmissions` is deprecated.** ShipKit uses the 3-step `reviewSubmissions` flow.
- **`READY_FOR_SALE` is not editable.** To push new metadata, bump `build.version.marketing` — ShipKit will auto-create a new version.
- **`sort` param is rejected on `/apps/{id}/appStoreVersions`.** If calling the SDK directly, filter client-side instead.
- **Rejected versions hold the version hostage.** After a 4.1(a) or 1.5 rejection, the `UNRESOLVED_ISSUES` reviewSubmission keeps your version "owned" and you can't create a new submission for it. ShipKit v0.2.4+ handles this by DELETEing the old submission's items, which releases the version. Terminal-state submissions (UNRESOLVED_ISSUES, COMPLETE) can't be canceled — Apple rejects `PATCH canceled=true` — but the item-deletion trick works.
- **READY_FOR_REVIEW orphans from partial submits.** If your `shipkit submit` fails mid-flow, you may leave a half-created submission. v0.2.4+ finds and cancels these automatically on the next attempt.
- **Resolution Center messaging is web-only.** Apple hasn't exposed it via the public ASC API. ShipKit puts your `metadata.review.notes` in the right place for the next reviewer, but if you need to reply to an existing rejection thread, you have to paste it at `appstoreconnect.apple.com` manually.
- **4.1(a) "Copycats" doesn't accept disclaimers.** If your metadata names a specific IP, franchise, or character — even with a "fan-made, unaffiliated" disclaimer — it gets rejected. Rewrite the metadata to describe the genre without naming the source. Your app's actual content (writing, artwork) can still be inspired by the genre.
- **Run from the project directory.** ShipKit's config loader searches `cwd` for `shipkit.yml`. If you have multiple projects, `cd` into each before running.
- **Expo: `appVersionSource: "remote"` in eas.json does not override hardcoded `buildNumber` in `app.json`.** Remove the hardcoded value so ShipKit's auto-increment can take over.
- **`WAITING_FOR_REVIEW` and `IN_REVIEW` submissions can't be double-submitted.** ShipKit will bail with a clear error. Cancel manually in ASC or wait for the current review to complete.

## Known Limitations

Works today: metadata push/pull, submit/resubmit with lifecycle cleanup, build, upload, status, per-app insights via the dashboard.

Not yet supported (pre-1.0):
- Screenshot upload (roadmap: v0.3)
- In-app purchase management (v0.3)
- TestFlight distribution (v0.3)
- Code signing / profile management (v0.4)
- Resolution Center messaging (not in Apple's public API)
- App-level metadata pull (only version-level fields come back)

## Dashboard

Web dashboard at **[shyguy.studio/shipkit/dashboard](https://shyguy.studio/shipkit/dashboard)** — connect your ASC API key (encrypted storage), view all your apps with Domino's-style pipeline trackers, submission history, and aggregate insights across all ShipKit users (Solo+ plan).

## Docs

Full documentation at **[shyguy.studio/shipkit/docs](https://shyguy.studio/shipkit/docs)**.

## Telemetry

ShipKit collects anonymous usage analytics (command name, success/fail, duration, OS) to improve the tool. No app names, API keys, bundle IDs, or file paths are ever collected.

```bash
shipkit telemetry status    # check if enabled
shipkit telemetry disable   # opt out
shipkit telemetry enable    # opt back in
```

## License

MIT — [Shy Guy Studio](https://shyguy.studio)