@shyguy_studio/shipkit
v0.2.5
Published
Programmatic App Store submission. One YAML, one command.
Maintainers
Readme
@shyguy_studio/shipkit
Programmatic App Store submission. One YAML config, one command to ship.
npm install -g @shyguy_studio/shipkit
shipkit init
shipkit shipWhat 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
npm install -g @shyguy_studio/shipkit
# Verify
shipkit --version # 0.2.4Or use without installing:
npx @shyguy_studio/shipkit@latest initQuick Start
# 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 shipCommands
| 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)
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: "[email protected]"
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:
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:
# 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 submitDon'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 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
buildanduploadcommands (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.
- Team Keys only. Individual keys return 401 on the ASC REST API. No workaround — regenerate as Team Key.
privacyPolicyUrlis 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 theappsresource. Settingapp.namein YAML only affects internal ShipKit logging. Usemetadata.locales.<locale>.nameto change the App Store display name. POST /appStoreVersionSubmissionsis deprecated. ShipKit uses the 3-stepreviewSubmissionsflow.READY_FOR_SALEis not editable. To push new metadata, bumpbuild.version.marketing— ShipKit will auto-create a new version.sortparam 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_ISSUESreviewSubmission 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 rejectsPATCH canceled=true— but the item-deletion trick works. - READY_FOR_REVIEW orphans from partial submits. If your
shipkit submitfails 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.notesin the right place for the next reviewer, but if you need to reply to an existing rejection thread, you have to paste it atappstoreconnect.apple.commanually. - 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
cwdforshipkit.yml. If you have multiple projects,cdinto each before running. - Expo:
appVersionSource: "remote"in eas.json does not override hardcodedbuildNumberinapp.json. Remove the hardcoded value so ShipKit's auto-increment can take over. WAITING_FOR_REVIEWandIN_REVIEWsubmissions 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 — 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.
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.
shipkit telemetry status # check if enabled
shipkit telemetry disable # opt out
shipkit telemetry enable # opt back inLicense
MIT — Shy Guy Studio
