Last Week

Week 49 ended with two unfinished promises. I picked Codex as the long-term Kotlin Multiplatform copilot and got the first iOS archive to upload through GitHub Actions, but that victory came with a warning label: hosted macOS runners eat ten times as many minutes, and I only had 2,000 free credits to burn. I also committed to finishing the App Store Connect onboarding so TestFlight could mirror the Android alpha track. That means fully populated metadata, screenshots that actually represent the product, and clarity on whether Apple will force Sign in with Apple on a business-facing tool that already has email-based authentication. This week was about paying down those debts so iOS progress stops being theoretical.

Self-Hosted Runners vs. TestFlight Red Tape

The video sounds like a rant because I immediately ran into both blockers. The hosted runner budget evaporated after four builds; each Compose Multiplatform archive takes 30-40 minutes, and every minute is billed at ten times the Linux rate. The only way to keep iOS builds in CI without torching January’s budget was to pull the pipeline onto my own hardware. That meant registering my studio Mac Mini as a self-hosted GitHub Actions runner, wiring its Xcode toolchain to the existing workflow, and tightening my cleanup scripts so a long-lived machine can behave like GitHub’s ephemeral environment.

Once the runner drama subsided, the second blocker surfaced: policy confusion. TestFlight is supposed to be a relaxed, internal testing program, yet Apple still requires all of the metadata, screenshots, and compliance statements that ship to the full App Store listing. That work is fine-I can shoot screenshots and write descriptions-but the bigger uncertainty is whether the review team will reject Shokken because it requires a login that does not yet include Sign in with Apple. My entire authentication stack is platform-agnostic by design. Forcing platform-specific auth would mean bespoke Swift glue and a bunch of operational overhead caused by Apple’s “hide my email” relay.

Rather than speculate, I’m preparing all of the marketing assets and submitting the current build without Apple login. If it sails through review, great-I can keep onboarding restaurants with the existing email-first flow. If it bounces back with the dreaded “Apps that require login must offer Sign in with Apple” rejection, then I will bite the bullet, implement both Apple and Google sign-in so the platforms stay symmetric, and treat it as the cost of doing business on iOS.

What does it mean in English?

I spent the week making sure iOS builds don’t bankrupt me and understanding what it takes to get TestFlight approval. GitHub charges ten times more for macOS build minutes, so I moved the entire CI/CD pipeline onto my own Mac and added guardrails that wipe the machine between runs. On the TestFlight side, I gathered the screenshots, descriptions, and policy answers that Apple wants and mapped out what happens if they demand Sign in with Apple. The app is still the same waitlist tracker, but now I have a path to keep shipping iOS builds without paying for every experiment.

Nerdy Details

Killing the macOS minute burn

GitHub gives every account 2,000 free CI minutes per month. On Linux those minutes stretch forever, but macOS runners are billed at a 10x multiplier. Each iOS build of Shokken takes roughly 35 minutes because Compose Multiplatform has to recompile Kotlin/Native frameworks, sync CocoaPods, and codesign the archive. Four builds was enough to burn the entire December allotment and strand the workflow. Rather than pause development until the meter resets, I registered my studio Mac Mini as a self-hosted runner with ./config.sh --url https://github.com/endian-dev/shokken --labels self-hosted,macos. That agent can accept every job the workflow dispatches, so pushes still run tests, build the Android artifacts, and archive the iOS app without touching GitHub’s macOS pool.

Self-hosted comes with obvious tradeoffs. Anyone who compromises the repo can execute arbitrary code on the machine that is running the runner. The mitigation for now is scope: I’m the only committer, the machine stays on a private network, and the runner is configured with a dedicated macOS user that has no admin rights. Long term I still plan to move back to hosted runners with a paid minute bundle for production, but this buys me enough runway to keep iterating without babysitting the usage meter.

Making the local runner reliable

GitHub’s hosted runners feel clean because they are freshly provisioned virtual machines. My personal Mac is the opposite: caches accumulate, derived data piles up, and stale Gradle artifacts trigger file locks that stop the workflow cold. The first self-hosted run passed, the second died inside :iosApp:bundleRelease because an old build folder still held write locks on Kotlin libraries. The fix was to treat cleanup as part of the workflow instead of something I remember to do manually. Each job now opens with a bash block that wipes old archives, DerivedData, and Gradle outputs:

rm -rf build/ iosApp/build ~/Library/Developer/Xcode/Archives/*
rm -rf ~/Library/Developer/Xcode/DerivedData
./gradlew clean --no-daemon

I also added a closing step that purges the temporary keychain and any provisioning profiles that Fastlane imports at runtime. Those two guardrails mimic GitHub’s ephemeral lifecycle closely enough that consecutive runs no longer inherit corrupted artifacts. If I eventually wrap the runner in a VM or container, these same commands can live in the entrypoint script so the workflow stays identical regardless of where it runs.

Preparing the metadata gauntlet

My naive assumption was that TestFlight would accept rough metadata because it only targets internal or alpha testers. Apple disagrees. Before I can invite anyone, App Store Connect wants the production-quality bundle of assets: localized descriptions, 6.5" and 5.5" screenshots for both dark and light modes, app previews, an accurate privacy policy URL, support contact info, and export compliance statements. I spent part of the week inventorying what already exists from the Play Store listing, mocking up new iPhone screenshots in the Figma marketing deck, and writing neutral descriptions that explain the waitlist flow without promising future features. That copy will live in the metadata/en-US folder inside the repo so I can regenerate it for future releases.

The Sign in with Apple tradeoffs

Shokken requires an operator login because it manages real guest lists and notifications. Apple’s guideline 4.8 says any app that uses a third-party login provider must also offer Sign in with Apple. On Android that rule is easy to satisfy because Google login uses the same Firebase project that already stores user identities. On iOS it is messy: Sign in with Apple lets users mask their email. That is hostile to B2B support workflows because a restaurant owner could file a ticket with one address while the account record holds Apple’s relay. It also complicates upcoming paid plans; reconciling invoices to a relay address instead of the actual restaurant email would create manual work every month.

I am not going to preemptively fork the authentication stack without being asked. The plan is to submit the existing build with email login, document the rationale inside the TestFlight notes (“internal testers are provisioned manually by the developer”), and wait for feedback. If review insists on Apple login, I will implement it alongside Google login so both mobile platforms stay symmetric. The Kotlin Multiplatform code will still keep authentication logic in shared modules; the platform-specific code will be limited to credential retrieval and token exchange. It is extra work, but it ensures iOS testers are not blocked once the app leaves internal review.

Next Week

Next week is all about turning this prep into an actual TestFlight invite. I need to finish the screenshot set, finalize the English copy, and answer Apple’s export, encryption, and privacy questionnaires inside App Store Connect. Once that metadata lands I’ll ship the latest archive through the self-hosted runner and submit it for review without Sign in with Apple. If it passes, I can finally publish the TestFlight link alongside the Android alpha instructions. If it fails, the entire week flips to building Apple (and matching Google) login so the policy box gets checked quickly. In parallel I’ll keep hardening the local runner by locking down permissions, scripting restarts, and documenting the cleanup routine so macOS builds stay predictable until I budget for hosted minutes again.