How to Build iPhone Apps Without a Mac — EAS + GitHub Codespaces
00要約Overview
Tools
- Apple Developer ProgramApp Store submission$99/yr
- EAS buildBuilds on cloud macOSFree
- GitHub CodespacesOne-time certificate stepFree
- PC (any)Browser, one time onlyalready owned
01物語Story
Situation
With the dev loop from 1.1.1, the check-and-fix cycle through Expo Go spins nicely. During development, no build is ever needed.
Complication
Shipping to the App Store is another story. Submitting to Apple means building the app, and building an iPhone app requires a Mac — an unavoidable fact of Apple's ecosystem.
Question
A build needs a Mac. Without owning one, how do you reach the App Store?
02解決Solution
Criteria
- Works without owning a Mac (don't buy one, don't borrow one, don't host one)
Answer
# First time only (creating certificates) PC browser → GitHub → GitHub Codespaces → EAS (build) # From then on (everything from the iPhone) iPhone → GitHub → GitHub Actions → EAS (build) iPhone → GitHub → GitHub Actions → EAS (submit) → App Store Connect → TestFlight → iPhone
A Mac is required. It just has to live in the cloud.
EAS (Expo Application Services) builds your iPhone app on macOS machines on their side. All you need at hand is a device that can fire the command — and an iPhone is enough.
Reason
The one-time PC browser session exists because of interactivity. The first build creates the signing credentials for the App Store, and that process asks Y/N questions interactively. The answers are basically all Y, but automating them in GitHub Actions means writing tedious timing-sensitive response code. For an operation that happens a few times a year at most, automation isn't worth it — so the first time is manual.
Even that manual step needs no Mac. Open GitHub Codespaces — the VS Code environment GitHub gives you in a browser — and run the commands from there. The personal free tier (120 core-hours/month) is plenty. In theory a phone browser would work, but a terminal on a phone screen really is too small, so this one step used a PC browser. Again: a PC, not a Mac.
From the second time on, no interaction is needed: run the GitHub Actions workflow from the iPhone and the build rolls. When it succeeds, submit from Actions the same way, and the app lands in App Store Connect and comes down to your device through TestFlight. Right up to the moment of submission, everything completes on the iPhone.
Options
- Xcode Cloud — Apple's own CI/CD. But the initial setup needs Xcode, i.e., a Mac, which contradicts the "own no Mac" criterion. And since the app is built on EXPO, EAS's affinity settled it.
- GitHub Actions macOS runners — you can build directly on a macOS runner. But macOS minutes bill at 10× the Linux rate, and the 2,000 free minutes/month evaporate. Never dared.
03結果Result
Good
Build and submit both live in EAS now, so management is unified. Development in EXPO (1.1.1) continues seamlessly right up to the doorstep of submission — all from a phone. The real find: "you need a Mac," the tallest barrier in iPhone development, turned out not to mean "you need to own a Mac."
Bad
The free tiers come with constraints. As of writing:
| Service | Free tier | Notes |
|---|---|---|
| EAS build | 15 iOS builds/mo (+15 Android) | Failed builds still count |
| GitHub Actions | 2,000 min/mo | Free plan, private repos |
Normal development never hits these ceilings. But debugging native issues with repeated builds eats them fast — and the painful part is that failed builds still consume the count. Three consecutive errors from a config mistake, and a fifth of your month is gone.
Follow-up
Free-tier EAS builds sit at low priority, and the queue can get absurdly long. Waiting is fine — but here's the trap: if Actions sits waiting for the build to finish, that waiting time also drains your 2,000 Actions minutes. Waiting changes nothing about the result, so the right move is to run eas build with --no-wait and end the workflow the moment the trigger fires.
The trade-off is that build and submit no longer chain in one workflow. So it's a two-step routine: confirm the build succeeded on the EAS dashboard, then run submit from Actions. --no-wait works on submit too.
With this, the vertical line of "build it and deliver it" is connected — enough to release. Once released, add the safety layer that separates dev and production delivery — 1.1.3 Safe EAS Update operation — to prevent accidental releases during maintenance. Then the story widens sideways: 1.2 Localization.
I've been triggering GitHub Actions by hand (to save Claude credits), but I'm starting to think it's better to ask Claude to trigger them too — actually, lately I do. When I run Actions by hand, nothing about it stays in Claude's memory, so the next time we talk it sometimes assumes "you haven't released yet, right?" To keep the app's status recorded in Claude's progress file, going through Claude — even at a small credit cost — keeps things from drifting. (Though just telling it "I built it" afterward works too.)
■ Reproduce it yourself
A note for anyone who wants to reproduce this setup. Not meant to be read straight through — the prompt shows a few lines and scrolls.
- Enroll in the Apple Developer Program ($99/yr, developer.apple.com/programs)
- Create an App ID in App Store Connect (create the app slot → note the ascAppId, a number)
- Issue an App Store Connect API Key (Users and Access → Integrations → App Store Connect API). Note the three:
.p8file, Key ID, Issuer ID (the .p8 can't be re-downloaded, so save it) - Add three GitHub Secrets:
APPLE_API_KEY(full .p8 text) /APPLE_API_KEY_ID/APPLE_API_KEY_ISSUER_ID
Then paste the following.
I want to build this app for iOS and be able to submit it to App Store Connect.
Use EAS Build to build and EAS Submit to submit, in a setup where from the
second time on I can run it via a GitHub Actions manual trigger (workflow_dispatch).
[What to build]
1. Add the items needed for building to app.json
- ios.bundleIdentifier (the value I registered in Apple Developer; I'll swap
it in later, so put a placeholder and note it in a comment)
- basic items like ios.supportsTablet / requireFullScreen
- version (start at 1.0.0; note in a comment the policy of bumping it each build)
2. eas.json
- a production profile
- submit.production.ios.ascAppId is a placeholder I'll fill in later
3. .github/workflows/eas-build-ios.yml (workflow_dispatch)
- auto-number buildNumber as YYYYMMDDHHMM and write it into app.json
- eas build --profile production --platform ios --non-interactive --no-wait
- pass Apple auth via env from Secrets (APPLE_API_KEY / APPLE_API_KEY_ID /
APPLE_API_KEY_ISSUER_ID)
4. .github/workflows/eas-submit-ios.yml (workflow_dispatch)
- write the APPLE_API_KEY secret out to a temp file (.p8) and inject into eas.json
- eas submit --platform ios --profile production --latest --non-interactive
[Important notes to leave as comments]
- Only the first build needs a manual run in an interactive shell (the certificate
prompt appears, and CI can't answer it). From the second build on, CI's
non-interactive mode gets through.
- Bump app.json version every build (to avoid Apple's duplicate-version error)
When you're done, tell me the exact commands to run the first build in Codespaces.
Use the following as a reference for what to tell me.
(these are NOT instructions for Claude Code, but commands I type in the Codespaces terminal)
# 1. Open the repo's Codespace (GitHub Code → Codespaces → Create)
# 2. Install EAS CLI and log in to Expo
npm install -g eas-cli
eas login # enter your Expo username / password interactively
# 3. Install dependencies
npm install --legacy-peer-deps
# 4. First build (answer "Yes" to the certificate prompts here)
eas build --platform ios --profile production
# → certificates are stored on EAS, so later runs go through non-interactively from CI
When you run the commands yourself, take a phone photo of the PC browser, ask Claude "what do I do?", and it walks you through everything. Mostly you just press Y. You have to create the app ID in App Store Connect (browser); ask Claude "do I have all the fields?" and it tells you.