DEV Community

Desert Sky Labs
Desert Sky Labs

Posted on

How I Got an Expo tvOS App to TestFlight From Windows Without Buying a Mac First

I got my tvOS app working without a Mac and I want to help you do it too! I spent like $400 in tokens on this and thousands of Github actions minutes and almost gave up multiple times, so let me save you the headaches and the money.

As a homeschool mom of 5 (ages 2-11) I'm hyper aware of all the AI slop out there. I set out to make my kids a slop-free TV experience that I could cater to our homeschool learning goals and my kids' pet projects. We already use the AppleTV for our regular screen time and for our Jellyfin server, so a tvOS app seemed like a natural place for this newbie to start. Wow was I wrong! I do not have a Mac, I've got a raspberry pi and a windows laptop.

I burned through GitHub's free 2,000 macOS runner minutes faster than I expected, spent hundreds of dollars in AI tokens, and got fooled by green CI runs that had not actually produced a usable TestFlight build.

If you are trying to do this from Windows, the docs get fuzzy right where the expensive mistakes start.

The internet got very unhelpful very quickly. Most advice either pointed back to Expo docs that skipped the ugly parts, or assumed I already owned a Mac and knew my way around Xcode.

A lot of the advice is either:

  • too vague to trust
  • written for people who already own a Mac
  • technically true but missing the part that was actually breaking

So here is the version I wish I could have found first.

The actual goal

I was building a real kids TV app with Expo and React Native.

The goal was pretty simple:

  • get the app building
  • get it uploaded
  • get it into TestFlight

The reality was... not that.

Where this went sideways

The biggest obstacle is that tvOS sits in an annoying middle ground.
It is close enough to iOS that people talk about it like it should mostly work.
But it is different enough that the weird parts matter a lot.

That means you can lose a lot of time in places like:

  • build tooling
  • asset requirements
  • App Store metadata
  • upload verification
  • credentials
  • versioning

And the worst part is that you can think you are done when you are not.

The trap that wasted the most time

A green CI run is not always a successful submission.

That one deserves to be said twice.

A green CI run is not always a successful submission.

I had runs that looked successful even though the upload step had not actually landed the build where it needed to go.
So if you are only watching the big green checkmark, you can waste a ridiculous amount of time celebrating the wrong thing.

What actually mattered for me

1. Treat tvOS like its own platform

Not like iOS with a couch.

That mindset shift helps a lot.
Because if you assume everything should behave like iOS, you keep debugging the wrong layer.

2. Clean native regeneration mattered

Stale native state can poison later builds.
If you are changing app metadata, build numbers, assets, or submission flow, be suspicious of old generated native files.

3. Upload verification mattered more than the workflow summary

This was the false-green problem.
I needed to verify the upload output itself, not just trust the workflow result.

4. Version and build number are not the same problem

Apple is very happy to let you learn this the annoying way.

5. Asset requirements are picky enough to ruin your afternoon

If your tvOS assets are wrong, Apple will absolutely let you find that out late.

The path that finally made sense

I am not putting the full copy-paste workflow in this free post, but I do want to tell you the shape of the path that finally stopped the bleeding:

  1. Use a GitHub Actions macOS runner, not a Linux runner
  2. Use Xcode 26+ on that runner, because that was the fix that finally got me past the Swift 6 wall
  3. Regenerate the native project cleanly before the build
  4. Make sure EXPO_TV=1 is actually set so you are building the tvOS variant
  5. Build on the runner with local EAS, so you can control the environment
  6. Expect EAS to still use the ios platform flag for this part, because tvOS is still awkward in the tooling
  7. Upload with xcrun altool using --type appletvos
  8. Fail the workflow unless the upload log explicitly confirms success

That was the high-level path.
For me, GitHub Actions plus xcrun altool was the successful path. The more generic EAS-only path created more confusion than it solved.
The exact workflow yaml, config snippets, and troubleshooting flow are what I put in the paid version.

What I would tell anyone doing this from Windows

If you already have an Expo or React Native app and you are trying to get to TestFlight without buying a Mac first, this is the order I would think about it in:

1. Make sure the app actually belongs on tvOS

This sounds obvious, but it matters.
If your app is mostly forms, settings screens, or touch-first interactions, tvOS may not be worth the build pain.
If your app is media-first, learning-first, or browse-and-play, it makes a lot more sense.

2. Get App Store Connect set up correctly before your first real build

This is one of the easiest ways to waste time.
Before you trigger expensive builds, make sure you have:

  • a tvOS app created in App Store Connect, not an iOS app
  • the correct bundle identifier
  • the numeric App Store Connect app ID
  • an App Store Connect API key
  • the API key's Key ID and Issuer ID
  • the .p8 file saved somewhere safe, because Apple only lets you download it once

If any of that is wrong, you can burn a lot of build time chasing a problem that was never in your app code.

3. Get the testing flow settled early

This helped more than I expected.

Before you obsess over app code, make sure the delivery path is at least understandable on a real Apple TV:

  • install the TestFlight app on the Apple TV
  • add or redeem the tester access early
  • watch where the build shows up
  • pay attention to the version and build numbers Apple is actually showing you

When my app showed up as incompatible, that was useful information. It ruled out some guesses and pointed back toward platform, provisioning, and submission issues. And when it finally moved from incompatible to installable, that was one of the clearest signs that the path was finally right. When the build number was not changing the way I expected, that was also a clue that the new build was not actually getting through the path I thought it was.

That is why I would work on the flow earlier than feels natural. Sometimes the bug is in the delivery pipeline not in the app.

4. Protect yourself from GitHub Actions minute burn

This is where I got hurt.

GitHub offers 2,000 free GitHub Actions minutes. Those will disappear faster than you think on macOS runners. A full run can easily take around 15 to 20 minutes, and when you are iterating on workflow syntax, auth, file paths, signing, invalid eas.json fields, or tvOS-specific config, the minutes pile up fast.

A few things I would do immediately:

  • use workflow_dispatch while you are stabilizing the workflow
  • set a GitHub billing limit before you start experimenting
  • do as much validation locally as you can before kicking off another macOS run
  • do not leave a broken poll loop chewing runner time for no reason (ask me how I know!)

The mistake was not just "GitHub is expensive." The mistake was using expensive macOS time to discover fixable config problems one run at a time.

5. Clean-regenerate native files when you change important tvOS config

If you change assets, metadata, bundle IDs, versioning, or native plugin config, do not assume the old generated iOS project is still trustworthy.

In my case, clean native regeneration mattered. Old native state can keep confusing you long after you think you fixed the real issue.

6. Treat version and build number as separate levers

This one is sneaky.

If Apple has already seen a version string, changing only the build number may not save you.
You need to understand both:

  • version is the release version users see
  • buildNumber is the internal increment for that version

The safe rule, if you are trying to stop wasting time, is this:
if Apple has already touched that version, bump the version before the next serious submission attempt.

7. Get the asset requirements right early

These are published in pieces elsewhere, but it helps to see them in one place.

Apple validates exact pixel dimensions. Close is not good enough.
And yes, the 2x assets matter.

tvOS app icons

Asset 1x 2x Required
App Icon Small 400x240 800x480 Yes
App Icon Large 1280x768 2560x1536 Yes

tvOS top shelf images

Asset 1x 2x Required
Top Shelf 1920x720 3840x1440 Deprecated, but may still be checked
Top Shelf Wide 2320x720 4640x1440 Yes

The practical takeaway:

  • create both 1x and 2x assets
  • use the exact dimensions above
  • do not assume reusing one file for multiple slots will pass validation
  • verify your asset catalog is actually generated correctly in the app bundle

8. Do not trust the workflow summary, trust the upload proof

If your workflow turns green but the upload step does not clearly prove success, you are not done.

For me, the important thing was checking the upload output itself.
The workflow needed to fail unless altool actually confirmed the archive upload succeeded, not just finish without an obvious crash.
If your logs do not contain a clear success message like No errors uploading archive, treat that run as suspect.

That one change saves real time, because it kills the false-green problem early.

What this post is and is not

This is not a full end-to-end course.
It is the honest summary of where the real pain showed up.

If you are looking for:

  • a broad introduction to tvOS development
  • a beginner coding tutorial
  • a claim that this whole process is smooth

This is not that.

If you are looking for:

  • a realistic path
  • hard-earned gotchas
  • the places the docs get fuzzy
  • a faster route than brute force trial and error

Then yes. This is for you.

If you want the cleaned-up kit

After fighting through this, I cleaned up the working path into a paid kit with:

  • the exact GitHub Actions workflow yaml
  • the config snippets
  • the asset checklist
  • the upload verification pattern
  • troubleshooting notes
  • the common mistakes I hit so you do not have to hit all of them yourself

If you are in the exact situation I was in, that version is for you.

If this free post got you most of the way there, great. Truly.
If it saved you time and you do not need the full kit, the coffee Diet Dr Pepper link is there too.

Final thought

This whole corner of the stack is still weirdly under-documented.
That means two things can be true at once:

  1. yes, it is possible
  2. yes, it is still more annoying than it should be

If you are in the middle of it right now, I get it. Dang.

I will keep cleaning this up as I go so the next person does not have to reconstruct the path from build logs and vibes.

Top comments (0)