Documentation

v0.2.4

Command reference, config format, SDK, and troubleshooting.

Installation#

No install required — use npx:

npx @shyguy_studio/shipkit@0.2.4 init

Or install globally:

npm install -g @shyguy_studio/shipkit@0.2.4
shipkit init

Verify the version:

shipkit --version
# 0.2.4

shipkit init#

Interactive setup wizard. Detects your project type (Xcode, XcodeGen, workspace), generates shipkit.yml.

shipkit init

Detects: .xcodeproj, .xcworkspace, project.yml (XcodeGen). Auto-extracts bundle ID, team ID, scheme name.

shipkit auth#

Validate your App Store Connect API credentials.

shipkit auth

Reads key from shipkit.yml, generates a JWT, and tests it against the ASC API. Reports which apps are accessible.

Requirements

You need a Team Key (not Individual Key) from App Store Connect → Users and Access → Integrations → Team Keys. Role must be Admin or App Manager.

shipkit build#

Build and archive your Xcode project. Produces an .ipa file.

shipkit build
shipkit build --clean
shipkit build --skip-export

Runs pre-build hooks (e.g. xcodegen generate), auto-increments build number from ASC, archives with xcodebuild, exports .ipa.

Options

--clean — clean before building
--skip-export — archive only, don't export .ipa

shipkit upload#

Upload an .ipa to App Store Connect.

shipkit upload
shipkit upload --ipa ./path/to/app.ipa
shipkit upload --skip-validation

Uses xcrun altool under the hood. Validates before uploading unless skipped.

shipkit metadata#

metadata push

Push metadata from your shipkit.yml to App Store Connect. Creates a new version if no editable version exists. As of v0.2.0, ShipKit pushes both version-level fields and app-level fields in a single command.

shipkit metadata push

Version-level fields (per locale): description, keywords, what's new, marketing URL, support URL, promotional text

App-level fields (per locale, via appInfoLocalizations): name, subtitle, privacy policy URL, privacy choices URL

Other fields: primary/secondary category (via appInfo relationships), App Review Information (notes, contact, demo account)

Note: Apple moved privacyPolicyUrl from version-level to app-level in 2024. ShipKit v0.2.1+ handles this automatically — the field stays at the same location in your YAML.

metadata pull

Pull current metadata from ASC to your terminal.

shipkit metadata pull

shipkit submit#

Submit (or re-submit) the current editable version for App Store review.

shipkit submit

Uses Apple's 3-step reviewSubmissions API: creates a reviewSubmission, attaches the version, marks submitted: true. Build must be attached and processed first.

Resubmission lifecycle (v0.2.4+)

If your version was previously rejected, ShipKit will automatically clean up the orphaned submission state before resubmitting:

1. Lists every existing reviewSubmission for the app

2. If one is in IN_REVIEW or WAITING_FOR_REVIEW, bails with a clear error

3. If one is in UNRESOLVED_ISSUES (rejected) and still holds your version, deletes its items to release the version (Apple won't let you cancel terminal-state submissions, but DELETEing items works)

4. If one is a READY_FOR_REVIEW orphan from a partial earlier submit, deletes its items + cancels it

5. Creates a fresh submission, attaches the version, marks submitted

Editable states for resubmission: PREPARE_FOR_SUBMISSION, REJECTED, METADATA_REJECTED, DEVELOPER_REJECTED, INVALID_BINARY.

shipkit submissions#

Inspect and manage individual reviewSubmissions. Useful for debugging stuck states.

submissions list

List every reviewSubmission for the current app, with state and submitted date.

shipkit submissions list

# Output:
#   c107d4cf-...  WAITING_FOR_REVIEW    4/10/2026, 3:36:56 AM
#   d868e031-...  COMPLETE              4/9/2026, 8:11:38 PM
#   5b490ebd-...  COMPLETE              4/7/2026, 10:50:51 PM

submissions cancel <id>

Explicitly cancel a stuck submission by ID. Only works on non-terminal states (READY_FOR_REVIEW). Terminal states (UNRESOLVED_ISSUES, COMPLETE) can't be canceled — but `shipkit submit` knows how to release versions from them.

shipkit submissions cancel 7970750e-fce2-4845-ac03-bfa9dc06848c

shipkit status#

Check your app's current review status.

shipkit status

Shows: app name, version, state (color-coded). States: PREPARE_FOR_SUBMISSION, WAITING_FOR_REVIEW, IN_REVIEW, PENDING_DEVELOPER_RELEASE, READY_FOR_SALE, REJECTED.

shipkit ship#

The full pipeline in one command.

shipkit ship
shipkit ship --dry-run
shipkit ship --skip-build
shipkit ship --skip-metadata
shipkit ship --skip-submit

Chains: hooks → build → upload → wait for processing → metadata push → submit. Each step can be skipped with flags.

--dry-run shows what would happen without executing anything.

shipkit telemetry#

ShipKit collects anonymous usage data (command name, success/fail, duration, OS). No app names, API keys, or personal data.

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

Config Reference#

Every field in shipkit.yml:

version: 1                     # config version

auth:
  key_id: "42NQL368GJ"         # ASC API Key ID
  issuer_id: "95e8ae19-..."    # ASC Issuer ID (Team Keys page)
  key_path: ./keys/AuthKey.p8  # path to .p8 file

app:
  bundle_id: com.example.app   # your app's bundle ID
  apple_id: "6761797247"       # optional, ASC app ID
  team_id: 5UF3Q334K6          # Apple Developer Team ID
  name: "My App"               # app name

build:
  project: MyApp.xcodeproj     # or workspace: MyApp.xcworkspace
  scheme: MyApp                # Xcode scheme
  configuration: Release       # Debug or Release
  export:
    method: app-store          # app-store | ad-hoc | development
  version:
    marketing: "1.0.0"         # CFBundleShortVersionString
    build: auto                # "auto" = latest from ASC + 1

metadata:
  default_locale: en-US
  locales:
    en-US:
      # App-level (pushed via appInfoLocalizations)
      name: "My App"
      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: "keyword1,keyword2"        # 100 char limit
      promotional_text: "..."              # 170 char limit
      marketing_url: "https://..."
      support_url: "https://..."
      whats_new: "Bug fixes."              # 4000 char limit

  # Optional: set primary/secondary category
  categories:
    primary: "REFERENCE"                   # ENTERTAINMENT | UTILITIES | etc.
    secondary: "BOOKS"

  # Optional: App Review Information (notes for the App Reviewer)
  review:
    first_name: "Chad"
    last_name: "Neal"
    email: "you@example.com"
    phone: "+1-555-0123"
    notes: |
      Hi reviewer,

      A short message explaining what this build adds, demo
      account credentials if needed, etc. Apple's reviewer reads
      this when they pick up your submission.

release:
  type: manual                 # manual | automatic | phased

hooks:
  pre_build:
    - "xcodegen generate"      # runs before build
  post_build:
    - "echo done"              # runs after build

Environment variables are interpolated: ${ASC_KEY_ID} reads from process.env.ASC_KEY_ID.

SDK#

Every ShipKit operation is available as a TypeScript import:

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

const kit = new ShipKit({
  keyId: "42NQL368GJ",
  issuerId: "95e8ae19-...",
  keyPath: "./keys/AuthKey.p8",
});

const app = await kit.apps.find("com.example.app");
const status = await kit.submissions.getStatus(app.id);
console.log(status.state); // "WAITING_FOR_REVIEW"

Modules: kit.apps, kit.appInfo, kit.builds, kit.metadata, kit.submissions

The appInfo module (added in v0.2.0) handles app-level metadata: name, subtitle, privacy URL, and primary/secondary categories. The submissions module handles the full reviewSubmission lifecycle including listing, cancelling, and item-deletion-based cleanup of stuck submissions.

After a Rejection#

App rejection is a normal part of the lifecycle. ShipKit v0.2.4+ handles the full resubmission flow without requiring you to drop into the App Store Connect web UI. Here's the recommended sequence:

1. Read the rejection

Apple emails you the rejection with the violated guideline number and a description. Don't panic — most rejections are metadata fixes, not code fixes.

2. Fix the issue

If the issue is in your app metadata (description, keywords, name, subtitle, screenshots, support URL), update shipkit.yml. If it's a code issue, fix the code and bump the build number.

3. Push the new metadata

shipkit metadata push

Pushes everything: version-level fields, app-level fields (name/subtitle/privacy URL), categories, and the App Review Information notes. Use the notes to acknowledge the rejection and explain what changed — Apple's reviewer reads this before re-reviewing.

4. Reply in Resolution Center (manual)

The Resolution Center messaging system isn't exposed via the public ASC API, so you have to paste your reply manually at appstoreconnect.apple.com/apps/{your-app} → click into the rejected version → look for the "Resolution Center" link in the rejection banner. This reply threads under the original rejection so the same review team sees your acknowledgment.

5. Resubmit

shipkit submit

ShipKit automatically detects orphaned and rejected submissions and cleans them up before creating a fresh one. See shipkit submit for the full algorithm.

6. Track the new submission

shipkit submissions list
shipkit status

Troubleshooting#

401 Authentication Error

You're using an Individual Key instead of a Team Key. Individual keys don't work with the ASC REST API. Generate a new key at App Store Connect → Users and Access → Integrations → Team Keys.

409 "Cannot be edited at this time"

You're trying to push metadata to a version that's already live (READY_FOR_SALE). Set build.version.marketing in your config to a new version number — ShipKit will create the new version automatically.

409 "privacyPolicyUrl is not an attribute on appStoreVersionLocalizations"

Apple moved the privacy policy URL from version-level to app-level (appInfoLocalizations). Upgrade to ShipKit v0.2.1+ which handles the new location automatically. The field stays at metadata.locales.<locale>.privacy_url in your YAML — only the underlying API call changed.

409 "Item is already present in another reviewSubmission"

A previous reviewSubmission is still "holding" your version. Usually this happens after a rejection (the rejected submission is in UNRESOLVED_ISSUES terminal state) or after a partial submit attempt left an orphan in READY_FOR_REVIEW. Upgrade to ShipKit v0.2.4+ — shipkit submit automatically detects these and releases the version by deleting their items before creating a new submission.

To inspect what's stuck: shipkit submissions list

403 "appStoreVersionSubmissions does not allow CREATE"

Apple deprecated this endpoint. ShipKit v0.1.0+ uses the newer reviewSubmissions API. Make sure you're on the latest version.

Build number conflict

Set build.version.build: auto in your config. ShipKit will query ASC for the latest build number and increment it. If you have a hardcoded buildNumber in app.json (Expo), remove it — it overrides auto-increment.

EAS submit fails silently

EAS submit errors are often vague. Common causes: duplicate build number, build not yet processed, or Apple API transient failure. Retry after a few minutes, or use shipkit upload directly.

"Version is in state REJECTED — can only submit from PREPARE_FOR_SUBMISSION"

This was a v0.2.1 bug. Upgrade to v0.2.2+ which allows resubmission from any editable state including REJECTED, METADATA_REJECTED, and DEVELOPER_REJECTED.

Changelog#

v0.2.4 — current

Resubmission lifecycle. Auto-detects stuck reviewSubmissions (UNRESOLVED_ISSUES, READY_FOR_REVIEW orphans) and releases the version by deleting their items before creating a fresh submission. Adds submissions list and submissions cancel <id> commands.

v0.2.3

Surface Apple's associated errors (multi-line, with field pointers) instead of just the primary error. Per-step error wrapping in submit so you know which step failed (create / attach / submit).

v0.2.2

Allow shipkit submit from rejected states (REJECTED, METADATA_REJECTED, DEVELOPER_REJECTED, INVALID_BINARY). Previously locked to PREPARE_FOR_SUBMISSION only.

v0.2.1

Removed deprecated privacyPolicyUrl from version-level push (Apple moved it to app-level). The field still works in your YAML — ShipKit now pushes it via appInfoLocalizations.

v0.2.0

App-level metadata. New AppInfoModule pushes name, subtitle, privacy URL, and categories via appInfoLocalizations and appInfos. New metadata.pushReviewDetail() pushes notes + contact info to appStoreReviewDetail. CLI metadata push now does version locale → app-info locale → categories → review detail in one pass.

v0.1.0

Initial release. CLI commands: init, auth, build, upload, metadata push/pull, submit, status, ship. SDK with apps, builds, metadata, submissions modules. Telemetry. JWT auth via .p8 keys. reviewSubmissions API.