DEV Community

Cathy Lai
Cathy Lai

Posted on

Release workflow for sole developer

If you’re a one‑person team with a few occasional testers, here’s a simple, durable process that balances speed (for your day‑to‑day iteration) with stability (when you need a new binary).


TL;DR

  • Keep a TestFlight “store” preview build as your main testing track for friends & family.
  • Ship day‑to‑day fixes via eas update (OTA) to the same channel as that preview build.
  • Use EAS “internal distribution” builds only for quick smoke tests on your own device, not for broader testing.

Why this setup works

TestFlight “store” preview builds (recommended for friends & family)

  • ✅ Easy install (no UDIDs), just a TestFlight link.
  • ✅ Works for both Internal (App Store Connect users) and External testers (up to 10k).
  • ✅ Apple’s analytics + crash data + build expiration handling.
  • ⚠️ Requires App Store Connect processing; External builds require Beta App Review.

Pair this with EAS Update and you’ll avoid cutting a new binary for every small fix:

# JS/asset-only changes to the same runtimeVersion
eas update --channel preview --message "Minor fix"
Enter fullscreen mode Exit fullscreen mode

EAS “internal distribution” builds (use sparingly)

  • ✅ Super fast to install on your device (no Apple processing).
  • ⚠️ Testers must share UDIDs; you must register devices (friction).
  • ⚠️ Per‑year device limits; no TestFlight analytics/crash data.

Best for: you doing quick smoke tests before creating a TestFlight build.


A simple, durable workflow

1) Keep one TestFlight preview track (store builds)

# Build a store-distribution binary and submit to App Store Connect
eas build --platform ios --profile preview
eas submit --platform ios --latest
Enter fullscreen mode Exit fullscreen mode
  • After processing, add the build to your Internal group (instant).
  • For External testers, add “What to Test” and submit for Beta App Review.

2) Iterate fast with OTA updates (no new binary)

# Publish JS/asset-only changes to the same channel targeted by that binary
eas update --channel preview --message "UI polish / copy tweaks"
Enter fullscreen mode Exit fullscreen mode

3) Only rebuild when:

  • You change native modules or config that requires a new binary.
  • You want to bump the build number (same marketing version) or the marketing version for a milestone.

4) Use an internal build for your own device when you need a rapid smoke test:

eas build --platform ios --profile internal
# Install from the Expo build page (register your device once if needed)
Enter fullscreen mode Exit fullscreen mode

Example eas.json (copy/paste)

{
  "cli": { "version": ">=16.20.1" },
  "build": {
    "preview": {
      "channel": "preview",
      "ios": {
        "buildConfiguration": "Release",
        "distribution": "store" <-- Apple App Store
      }
    },
    "internal": {
      "channel": "preview",
      "ios": {
        "buildConfiguration": "Release",
        "distribution": "internal" <-- Expo ad-hoc 
      }
    },
    "production": {
      "channel": "production",
      "ios": {
        "buildConfiguration": "Release",
        "distribution": "store"
      }
    }
  },
  "submit": {
    "preview": {
      "ios": {},
      ...
    },
    "production": {
      ...
    }
}
Enter fullscreen mode Exit fullscreen mode
  • preview = your TestFlight track (store builds + OTA to preview)
  • internal = fast ad‑hoc builds for your device
  • production = App Store release track (+ OTA to production)

Versioning & build numbers (iOS)

  • Small JS‑only fixes → don’t bump version; use OTA.
  • New binary, same version → bump ios.buildNumber only (e.g., 25 → 26).
  • Bigger release → bump version (e.g., 1.0.3 → 1.0.4) and reset ios.buildNumber to "1" (or your convention).
// app.config.ts
export default {
  expo: {
    version: "1.0.3",
    ios: { buildNumber: "26" },
    android: { versionCode: 26 },
    runtimeVersion: { policy: "sdkVersion" }
  }
}
Enter fullscreen mode Exit fullscreen mode
export default {
    expo: {
        name: "PlanetFam Quiz",
        slug: "my-app",
        version: "1.0.3", <--- only become 1.0.4 if major
        ...
        runtimeVersion: { policy: "sdkVersion" },
        ios: {
            supportsTablet: true,
            bundleIdentifier: "com.cathyapp1234.my-app",
            buildNumber: "28" <--- only increase for new build
        },
        android: {
            package: "com.radiantleaf.planetfam",
            versionCode: 28, <--- only increase for new build, same as above
         ....
        },

Enter fullscreen mode Exit fullscreen mode

Rule: Apple requires buildNumber to strictly increase for a given version. When you change version, you can reset buildNumber.


Channels & OTA (don’t mix them up)

  • A binary targets a channel (e.g., preview).
  • All OTA updates must be published to the same channel to reach that binary’s users:
eas update --channel preview --message "Fix trophy UI"
Enter fullscreen mode Exit fullscreen mode
  • Changing channel requires a new build (so the binary points at the new channel).

Quick decision guide

  • “Just a text fix / minor JS polish?” → OTA (eas update).
  • “Upgraded a native package / changed config?” → Build + Submit.
  • “Need to sanity check on my phone in 5 minutes?” → Internal build for you only.
  • “Want friends & family to test?” → TestFlight (store) preview build + OTA updates to preview.

This approach keeps your testing fast and your releases tidy, without burning time on unnecessary rebuilds. 🚀

Top comments (0)