<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jackson</title>
    <description>The latest articles on DEV Community by Jackson (@maclessdev).</description>
    <link>https://dev.to/maclessdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4079628%2F4c7c384d-c201-47fa-bdc3-887f5b74d44f.png</url>
      <title>DEV Community: Jackson</title>
      <link>https://dev.to/maclessdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maclessdev"/>
    <language>en</language>
    <item>
      <title>Set it up once. Stop thinking about it.</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Wed, 19 Aug 2026 20:38:34 +0000</pubDate>
      <link>https://dev.to/maclessdev/set-it-up-once-stop-thinking-about-it-fk0</link>
      <guid>https://dev.to/maclessdev/set-it-up-once-stop-thinking-about-it-fk0</guid>
      <description>&lt;p&gt;The whole pitch of Macless is that you shouldn't need a Mac, a rented cloud Mac, or a monthly CI subscription to ship an iOS or Android app — GitHub's free build minutes and a workflow file do the job Xcode used to. That part's been true since Citolex shipped on it. But there was a second, quieter kind of manual work I hadn't fully gotten rid of: generating the signing certificates and keystores in the first place, pasting the right base64 blobs into the right GitHub secrets, and then just... remembering that certificates expire, on your own, with no reminder, until a build fails the week you actually needed it to work.&lt;/p&gt;

&lt;p&gt;That's the part I went after this week. Not the pipeline — the setup and the upkeep around it. Three things, all live in the template now.&lt;/p&gt;

&lt;h2&gt;
  
  
  A wizard that runs the commands for you
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;generate_signing_secrets.sh&lt;/code&gt; already handled iOS. It now walks through Android too — generating a real keystore with &lt;code&gt;keytool&lt;/code&gt;, base64-encoding it, and telling you exactly which GitHub secret each value goes into. If you have the &lt;code&gt;gh&lt;/code&gt; CLI installed, it'll offer to push the secrets straight to your repo for you, so there's no copy-pasting eight values into a settings page by hand. And when it's done, it can hand off straight into Signing Doctor to confirm the keystore it just built is actually valid — not "the script exited zero," but an actual check that it works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./scripts/generate_signing_secrets.sh
→ generates the keystore, encodes it, shows you what to &lt;span class="nb"&gt;paste&lt;/span&gt;
→ offers to push it to GitHub &lt;span class="k"&gt;for &lt;/span&gt;you via gh
→ hands off to Signing Doctor to confirm it&lt;span class="s1"&gt;'s good
no manual keytool flags, no guessing which secret name goes where.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Signing Doctor now reads Android keystores
&lt;/h2&gt;

&lt;p&gt;Signing Doctor is the free diagnostic tool I built alongside Macless — point it at your signing material and it tells you what's actually wrong, instead of you guessing from a cryptic Xcode or Gradle error. It's always checked iOS certificates and provisioning profiles. It now checks Android keystores the same way: alias, validity window, whether the password you think you have actually opens it. Same tool, same free download, one more thing it won't let slip past you.&lt;/p&gt;

&lt;h2&gt;
  
  
  A workflow that checks before you ship, not after
&lt;/h2&gt;

&lt;p&gt;The one I think matters most: &lt;code&gt;check-expiry.yml&lt;/code&gt;, a scheduled GitHub Actions workflow that runs weekly against your own signing secrets and tells you if anything's getting close to expiring — before you're mid-release and a build starts failing for a reason that has nothing to do with your code. If you've got a webhook set up, it'll ping you directly. If not, it still writes the result to the workflow's own summary, so it's there the next time you look.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why this is the part that actually saves you: a broken build from bad code is annoying but obvious — you know immediately, and you know why. An expired certificate is worse, because it looks like everything's fine until it isn't, usually at the least convenient moment. That's the failure mode this closes off. Not "the pipeline works," but "the pipeline keeps working without you having to check on it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why this is different from the alternatives
&lt;/h2&gt;

&lt;p&gt;Doing this by hand in Xcode, you get none of this — expiry is something you find out about when a build fails, and there's no diagnostic tool telling you why. Paying for a managed CI service, you might get some of it, but you're paying monthly for the privilege, indefinitely, for a pipeline you don't own. Macless is still a one-time $99 (or $39 for Android-only) — no subscription — and now that includes a setup that walks you through it, a free tool that tells you what's wrong when something is, and a workflow that watches your certificates so you don't have to remember to.&lt;/p&gt;

&lt;p&gt;All three are already in the template if you've bought it — pull the latest and they're there, no repurchase needed. Signing Doctor is free either way, iOS or Android, whether or not you're using the rest of Macless.&lt;/p&gt;




&lt;p&gt;The build pipeline, the setup wizard, the expiry checks, and a free Signing Doctor for whenever something looks off — all in one $99 template, no subscription. &lt;a href="https://macless.dev/#pricing" rel="noopener noreferrer"&gt;See what's included →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>android</category>
      <category>githubactions</category>
      <category>cicd</category>
    </item>
    <item>
      <title>A header scan says 16/16. Here's what it can't see.</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Tue, 18 Aug 2026 01:48:57 +0000</pubDate>
      <link>https://dev.to/maclessdev/a-header-scan-says-1616-heres-what-it-cant-see-14ab</link>
      <guid>https://dev.to/maclessdev/a-header-scan-says-1616-heres-what-it-cant-see-14ab</guid>
      <description>&lt;p&gt;A few days after &lt;a href="https://dev.to/maclessdev/how-to-build-and-ship-an-ios-app-without-a-mac-17o5"&gt;how I built and shipped an iOS app without a Mac&lt;/a&gt; went up here, a reader named Amit Feldman left a comment with an actual finding, not a vague warning: macless.dev was fast and the on-page SEO was tidy, but HSTS, Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy were all missing. Six headers, zero excuses — the site's on GitHub Pages, and Pages doesn't let you set custom response headers at all. That's just how the platform works, and it's an easy thing to never notice if nobody runs the scan.&lt;/p&gt;

&lt;p&gt;The fix was straightforward once it was named: keep the site on Pages, put Cloudflare in front of the custom domain, and add the header set with a Response Header Transform Rule. HSTS is a toggle under Edge Certificates. None of it touches the deploy workflow. Amit re-scanned twice more as things landed — first the five easy headers, then a properly tight Content-Security-Policy (&lt;code&gt;default-src 'self'; script-src 'none'&lt;/code&gt;, since the page ships zero JavaScript, nothing to break) — and macless.dev sits at 16 passed, 0 warnings, 0 failures now. He's writing that before/after up as its own case study, which is a nicer way to end up getting linked than most.&lt;/p&gt;

&lt;p&gt;His follow-up point was the more useful one. A header scan reads what a browser receives — it says nothing about what a CI pipeline does with your signing certificates. Macless &lt;em&gt;is&lt;/em&gt; a signing pipeline: GitHub Actions, code-signing certificates, App Store Connect and Google Play credentials. A header scan can't tell you whether a cert is sitting in the repo, whether a workflow echoes a secret into its log, or whether a token is scoped wider than it needs to be. That's a fair distinction between two different layers, so rather than take anyone's word for either layer, I went and checked the second one directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "actually checked" means here
&lt;/h2&gt;

&lt;p&gt;Not a policy review. A search of the real git history of both &lt;code&gt;appledev&lt;/code&gt; (Citolex's source) and the Macless product template, plus a read-through of every workflow file line by line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full commit history (not just current files) searched for any certificate, private key, or keystore ever committed and later removed.&lt;/li&gt;
&lt;li&gt;Every workflow checked for secrets interpolated directly into shell commands versus passed through &lt;code&gt;env:&lt;/code&gt; (the safer pattern — GitHub can mask an env-sourced secret in logs; a value pasted straight into a &lt;code&gt;run:&lt;/code&gt; string is both a log-exposure and shell-injection risk).&lt;/li&gt;
&lt;li&gt;Checked for &lt;code&gt;set -x&lt;/code&gt;/&lt;code&gt;set -ex&lt;/code&gt;, which would dump every command — including expanded secret values — into the public build log.&lt;/li&gt;
&lt;li&gt;Checked what triggers each workflow. &lt;code&gt;pull_request_target&lt;/code&gt; combined with secrets is the classic way a fork PR exfiltrates credentials from a public repo; none of these workflows use it.&lt;/li&gt;
&lt;li&gt;Traced exactly where decoded certs and keystores get written during a build — ephemeral runner temp storage that dies with the job, or the repo working directory that could get committed or accidentally artifact-uploaded.&lt;/li&gt;
&lt;li&gt;Checked the actual GitHub Actions secret scoping this template's own docs tell you to set up (App Store Connect API key role, Play Console service account role).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What was already clean
&lt;/h2&gt;

&lt;p&gt;No certificate, key, or keystore file has ever been committed to either repo, at any point in their history. Secrets are passed through &lt;code&gt;env:&lt;/code&gt; blocks, not interpolated into shell strings. Nothing sets &lt;code&gt;-x&lt;/code&gt;. Every workflow triggers on &lt;code&gt;push&lt;/code&gt; or &lt;code&gt;workflow_dispatch&lt;/code&gt; only — nothing runs on a fork's pull request, so there's no path for an outside contributor to exfiltrate a secret through a crafted PR. Decoded cert and keystore material only ever lands in the runner's ephemeral temp directory, never the repo itself, and the artifact-upload steps grab specific screenshot files, not whole build directories. The documented credential roles (App Store Connect "App Manager," Play Console "Release manager") were already scoped down from full admin access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two real things, both fixed
&lt;/h2&gt;

&lt;p&gt;Not everything was already perfect — two genuine, if minor, gaps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. No explicit &lt;code&gt;permissions:&lt;/code&gt; block.&lt;/strong&gt; None of these workflows push commits, open PRs, or otherwise need to write back to the repo — they build, sign, and upload. Without an explicit block, &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; inherits whatever the repo or org default is, which can be broader than a workflow actually needs. Fixed by adding this to the top of every workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. No &lt;code&gt;.gitignore&lt;/code&gt; for signing material.&lt;/strong&gt; SIGNING.md and ANDROID.md walk you through generating a real certificate or keystore with &lt;code&gt;openssl&lt;/code&gt;/&lt;code&gt;keytool&lt;/code&gt;. Nothing in the docs stopped someone from running those commands inside their project folder, and a plain &lt;code&gt;git add .&lt;/code&gt; afterward would have committed a real private key. Fixed two ways: a &lt;code&gt;.gitignore&lt;/code&gt; now ships with the template covering &lt;code&gt;*.p12 *.key *.cer *.pem *.mobileprovision *.keystore *.jks *_base64.txt&lt;/code&gt;, and both docs now say plainly to generate this material from a scratch folder outside the repo, not inside it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you're building something similar:&lt;/strong&gt; both of these are worth checking in any CI pipeline that handles signing material, not just this one. An explicit &lt;code&gt;permissions:&lt;/code&gt; block costs three lines and closes off a class of supply-chain risk for free. A &lt;code&gt;.gitignore&lt;/code&gt; that anticipates exactly which files your setup docs are about to tell someone to generate is cheap insurance against the single most common way a real credential ends up in a public repo — someone followed the instructions correctly, in the wrong directory.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where this leaves things
&lt;/h2&gt;

&lt;p&gt;Both CI fixes are live now - in Citolex's own repo and in the Macless product template everyone downloads. The header layer Amit caught is fixed and independently reverified at 16/16, and he wrote the whole before/after up as &lt;a href="https://dev.to/amitfeldman/all-six-security-headers-fixed-in-a-day-the-macless-beforeafter-5343"&gt;its own case study&lt;/a&gt;. Worth being straightforward about the rest of his message too: alongside the case-study offer, he also pitched his paid audit services - a &lt;a href="https://afeldman2.gumroad.com/l/zxpluh?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=casestudy-macless" rel="noopener noreferrer"&gt;Launch-Ready Audit&lt;/a&gt; ($99) and a &lt;a href="https://afeldman2.gumroad.com/l/wmdfxb?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=casestudy-macless-deepdive" rel="noopener noreferrer"&gt;Deep Dive&lt;/a&gt; ($149) - covering exactly this CI/secrets layer. Given he'd already found three real things for free, that's not an unreasonable thing to charge for; I just happened to be able to check it myself directly, in about twenty minutes, using the repos' own git history rather than a read-only link handed to someone else. Two real, minor issues turned up, same as the header pass did. If you want the deeper, ongoing version of what he did here for free, his audits are worth a look. Thanks are owed either way: the header catch was real, the nudge to check the layer underneath it was a good one, and the case study is a fair trade for both.&lt;/p&gt;




&lt;p&gt;SIGNING.md and ANDROID.md now warn you before you generate anything, every workflow ships with a scoped-down &lt;code&gt;permissions:&lt;/code&gt; block, and the &lt;code&gt;.gitignore&lt;/code&gt; is there as a backstop either way — all part of &lt;a href="https://macless.dev" rel="noopener noreferrer"&gt;Macless&lt;/a&gt;, a one-time template for shipping an iOS (and now Android) app through GitHub Actions without a Mac.&lt;/p&gt;

</description>
      <category>security</category>
      <category>github</category>
      <category>cicd</category>
      <category>ios</category>
    </item>
    <item>
      <title>The actual cost of shipping an iOS app in 2026</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Sun, 16 Aug 2026 02:55:04 +0000</pubDate>
      <link>https://dev.to/maclessdev/the-actual-cost-of-shipping-an-ios-app-in-2026-4242</link>
      <guid>https://dev.to/maclessdev/the-actual-cost-of-shipping-an-ios-app-in-2026-4242</guid>
      <description>&lt;p&gt;"How much does it cost to put an app on the App Store" gets answered inconsistently online because most answers either only count Apple's fee, or only count hardware, or quietly assume you're renting expensive cloud infrastructure you don't actually need. Here's every cost, split into what's mandatory and what's a choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mandatory: Apple Developer Program — $99/year
&lt;/h3&gt;

&lt;p&gt;This is the one cost nobody can avoid. To submit any app to the App Store — free or paid, one app or fifty — you need an active Apple Developer Program membership, which is $99/year, billed annually, direct to Apple. There's no one-time version and no way around it.&lt;/p&gt;

&lt;p&gt;(There is a free-tier Apple ID for personal on-device testing without paying this, but it doesn't let you submit to TestFlight external testers or the App Store — for an actual public release, the $99/year membership is required.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Required, but where you have genuine choices: building and signing
&lt;/h3&gt;

&lt;p&gt;To submit a build, something has to run Xcode's command-line signing and archive tools — that part isn't optional. Where you have a choice is what runs it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Recurring?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Buy a Mac&lt;/td&gt;
&lt;td&gt;~$799+ (Mac mini, entry price)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rent a cloud Mac&lt;/td&gt;
&lt;td&gt;~$20–100+/month&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions, public repo&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions, private repo&lt;/td&gt;
&lt;td&gt;$0 up to a monthly allowance, then per-minute&lt;/td&gt;
&lt;td&gt;Only if you exceed the free allowance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The short version on that last row: on a public repo, this line item can legitimately be $0, indefinitely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optional or one-time: the things people assume cost more than they do
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;App Store screenshots and marketing assets. You can generate these yourself for free — from the Simulator or a physical device — no paid tooling required.&lt;/li&gt;
&lt;li&gt;A physical iPhone for testing. Not strictly required to submit, but you'll want one to sanity-check the finished app before release.&lt;/li&gt;
&lt;li&gt;TestFlight itself. Free, included in the $99/year membership.&lt;/li&gt;
&lt;li&gt;App Store listing itself. Free — no fee to list an app beyond the membership.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Adding it up
&lt;/h3&gt;

&lt;p&gt;For someone shipping a side project on a public repo, using GitHub Actions instead of a Mac or a cloud rental:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Apple Developer Program ....... $99/year  (mandatory)
Build &amp;amp; sign (public repo CI) ..... $0    (GitHub Actions)
Screenshots / assets .............. $0    (self-generated)
TestFlight / listing .............. $0    (included)
-----------------------------------------
Total to actually ship ......... $99/year
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that to the same app built with a purchased Mac (~$898 first year, then $99/year after) or a rented cloud Mac (~$339–1,299 first year, then that same range every year after). The gap is entirely in the "how do I run Xcode" line item.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth flagging plainly:&lt;/strong&gt; the $0 build-cost row above is specific to public repositories. Keeping your code private doesn't change how the pipeline works or expose anything extra — it just switches you from unlimited free minutes to a metered monthly allowance. Still usually cheaper than a Mac or cloud rental, just not literally free.&lt;/p&gt;

&lt;p&gt;I built the pipeline behind this shipping &lt;a href="https://citolex.com" rel="noopener noreferrer"&gt;Citolex&lt;/a&gt; specifically to avoid a recurring Mac-rental bill on top of Apple's own fee. Packaged as a one-time $99 template at &lt;a href="https://macless.dev" rel="noopener noreferrer"&gt;macless.dev&lt;/a&gt; — not a subscription.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>indiehackers</category>
      <category>github</category>
      <category>mobiledev</category>
    </item>
    <item>
      <title>GitHub Actions' free macOS minutes, explained</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Sun, 16 Aug 2026 02:53:18 +0000</pubDate>
      <link>https://dev.to/maclessdev/github-actions-free-macos-minutes-explained-33p7</link>
      <guid>https://dev.to/maclessdev/github-actions-free-macos-minutes-explained-33p7</guid>
      <description>&lt;p&gt;GitHub Actions is GitHub's built-in CI/CD system — it spins up a fresh virtual machine, runs whatever commands you tell it to, and tears the machine down when it's done. It supports Linux, Windows, and macOS runners. The macOS runners are the interesting part here, because they're actual macOS machines with Xcode's command-line build tools available, which means they can build and sign iOS apps — not just run tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  The headline rule: public repos are free
&lt;/h3&gt;

&lt;p&gt;On a public repository, standard GitHub-hosted runner minutes — including macOS — don't cost anything, on any plan, including the free plan. It's a genuine free tier, not a trial or a limited allowance that runs out.&lt;/p&gt;

&lt;p&gt;One nuance: this covers standard runners. GitHub also offers "larger runners" (more CPU/RAM) — those are billed regardless of repo visibility. A default macOS build for signing and archiving a typical app doesn't need one, so this rarely matters in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  What changes on a private repo
&lt;/h3&gt;

&lt;p&gt;If your repository is private, you get a monthly allowance of free minutes instead of unlimited free usage:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Included minutes / month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;3,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team&lt;/td&gt;
&lt;td&gt;3,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The number that actually matters for iOS builds is how fast macOS runners burn through that allowance.&lt;/p&gt;

&lt;h3&gt;
  
  
  macOS minutes cost roughly 10x
&lt;/h3&gt;

&lt;p&gt;GitHub applies a multiplier against your included minutes: Linux runners run at the baseline rate, macOS runners run at roughly 10x that rate. A 6-minute macOS build eats through the same allowance as roughly 60 minutes of Linux CI.&lt;/p&gt;

&lt;p&gt;Applied to the table above, a Free-plan private repo effectively gets around 200 macOS-runner-minutes worth of free build time per month before you're billed per minute past it (Pro/Team works out to roughly 300).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The practical upshot: if you're fine building in the open, a public repo gets you unlimited macOS build minutes at zero cost, indefinitely. If you'd rather keep the code private, everything about the pipeline still works the same way — you're just drawing from a metered allowance instead of an unlimited one. Repo visibility only changes CI minute pricing; your signing certificates and credentials live in encrypted GitHub secrets either way, never exposed regardless of visibility.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What this doesn't cover
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Apple Developer Program fee. Still $99/year, paid directly to Apple, completely separate from anything GitHub does.&lt;/li&gt;
&lt;li&gt;Storage and artifact retention. A distinct line item if you're archiving builds long-term, not usually a concern for a simple build → sign → upload pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Numbers on GitHub's side can shift — this reflects GitHub's billing docs as of when this was written, worth a check against &lt;a href="https://docs.github.com/en/billing/concepts/product-billing/github-actions" rel="noopener noreferrer"&gt;GitHub's current billing docs&lt;/a&gt; if it's been a while.&lt;/p&gt;

&lt;p&gt;Full pipeline built around this free tier at &lt;a href="https://macless.dev" rel="noopener noreferrer"&gt;macless.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>github</category>
      <category>cicd</category>
      <category>ios</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why your TestFlight build doesn't show up after a successful upload</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Sun, 16 Aug 2026 02:46:14 +0000</pubDate>
      <link>https://dev.to/maclessdev/why-your-testflight-build-doesnt-show-up-after-a-successful-upload-419f</link>
      <guid>https://dev.to/maclessdev/why-your-testflight-build-doesnt-show-up-after-a-successful-upload-419f</guid>
      <description>&lt;p&gt;You push, the build runs, the archive and export steps succeed, the upload step succeeds, the workflow finishes green. You open App Store Connect to check on it and TestFlight shows... the previous build. Not a failed build. Not a processing build. Nothing new at all.&lt;/p&gt;

&lt;p&gt;This one is genuinely confusing the first time it happens, because there's no error anywhere — everything you can see reports success.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's actually happening
&lt;/h3&gt;

&lt;p&gt;App Store Connect silently rejects an upload if it reuses the same marketing version &lt;em&gt;and&lt;/em&gt; build number as a build you've already uploaded — even one from weeks ago. It considers that pair a duplicate. But the upload tool (&lt;code&gt;xcrun altool&lt;/code&gt;, or the newer transporter flows) reports a successful &lt;em&gt;file transfer&lt;/em&gt; regardless — it uploaded the bytes fine, it just didn't tell you App Store Connect threw the result away on the other end.&lt;/p&gt;

&lt;p&gt;So the failure genuinely happened, it's just invisible at the layer your CI log is watching.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Make sure your build number (&lt;code&gt;CURRENT_PROJECT_VERSION&lt;/code&gt; in Xcode's build settings) changes on every single upload, even when your marketing version stays the same across several builds while you're iterating.&lt;/p&gt;

&lt;p&gt;The reliable way to do this in GitHub Actions is to use GitHub's own per-workflow run counter instead of anything you maintain by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcodebuild archive &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;CURRENT_PROJECT_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUILD_NUMBER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ...

&lt;span class="c"&gt;# where, earlier in the workflow:&lt;/span&gt;
&lt;span class="nb"&gt;env&lt;/span&gt;:
  BUILD_NUMBER: &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;{ github.run_number &lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;github.run_number&lt;/code&gt; increments by one on every run of that specific workflow, forever, with no state you have to track yourself. The actual root cause most of the time is someone customizing the archive step and dropping the dynamic build number in the process, usually while debugging something unrelated.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you're already using a dynamic build number and this is still happening: double check the substitution is actually reaching the archive command — a common way this breaks is the environment variable being set in the wrong step or shell context, so $BUILD_NUMBER silently expands to empty.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How to confirm this is actually your problem
&lt;/h3&gt;

&lt;p&gt;In App Store Connect, look at "Build Activity" or the processing history rather than just the current builds list — a duplicate-version upload sometimes doesn't even appear there. Worth separately ruling out: processing does take a few minutes even on a successful upload, so if it's only been a minute or two, it may simply not have finished yet.&lt;/p&gt;

&lt;p&gt;The Macless workflow file sets &lt;code&gt;CURRENT_PROJECT_VERSION&lt;/code&gt; from &lt;code&gt;github.run_number&lt;/code&gt; by default, so this failure mode doesn't happen unless you specifically override it. Full details at &lt;a href="https://macless.dev" rel="noopener noreferrer"&gt;macless.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>github</category>
      <category>cicd</category>
      <category>testflight</category>
    </item>
    <item>
      <title>Fix "No signing certificate matching team ID" in GitHub Actions</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Sun, 16 Aug 2026 02:44:29 +0000</pubDate>
      <link>https://dev.to/maclessdev/fix-no-signing-certificate-matching-team-id-in-github-actions-4pd2</link>
      <guid>https://dev.to/maclessdev/fix-no-signing-certificate-matching-team-id-in-github-actions-4pd2</guid>
      <description>&lt;p&gt;If you're building an iOS app in GitHub Actions (or any headless CI, really) and your archive/export step fails with something like &lt;code&gt;No signing certificate "iOS Distribution" matching team ID "XXXXXXXXXX" with a private key was found&lt;/code&gt;, the good news is this is almost never a code problem. It's a mismatch somewhere between what Apple's portal has on file and what your CI job is trying to use.&lt;/p&gt;

&lt;p&gt;In practice it's one of three things. Check them in this order — it's the order they're easiest to rule out in.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Check whether the certificate actually imported
&lt;/h3&gt;

&lt;p&gt;Before anything else, open the "Import signing certificate" (or equivalent keychain-setup) step in your Actions log. It prints the identities it actually found after importing your .p12 file — something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;1) AB12CD34EF... "Apple Distribution: Your Name (XXXXXXXXXX)"
     1 valid identities found
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that list is empty, or the identity isn't there at all, stop looking at provisioning profiles — the certificate itself didn't import. Usual causes: the base64-encoded .p12 secret got truncated or corrupted when it was pasted into GitHub Secrets, or the .p12 password secret is wrong. Re-export the certificate fresh rather than debugging the existing value.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Confirm the certificate and profile actually match
&lt;/h3&gt;

&lt;p&gt;A provisioning profile isn't generic — it's generated against one specific signing certificate. If the identity from step 1 did import successfully, the next most common cause is that the certificate on Apple's portal was regenerated (often because the old one expired) without regenerating the profile that references it. Profiles pin to a certificate's public key; once that certificate is gone, every profile built against it silently stops working, even though the profile itself hasn't "expired" and still shows as valid in the portal.&lt;/p&gt;

&lt;p&gt;The fix: in Apple's developer portal, open the provisioning profile and regenerate it (this re-links it to your current active certificate), then re-download it and update the base64 secret your workflow uses.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Check the profile name and bundle ID match exactly
&lt;/h3&gt;

&lt;p&gt;The third cause is the most tedious to catch because it fails silently on typos. Two things have to match character-for-character, including case and whitespace, between Apple's portal and your CI configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The provisioning profile's name, as typed into your IOS_PROVISIONING_PROFILE_NAME variable/secret.&lt;/li&gt;
&lt;li&gt;The bundle ID in IOS_BUNDLE_ID — it has to match the App ID the profile was actually generated for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copy both values directly out of the Apple Developer portal rather than retyping them from memory — that's the single easiest place a mismatch sneaks in.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you've checked all three and it's still failing: double-check you're not mixing up an "Apple Development" certificate with an "Apple Distribution" one — TestFlight and App Store builds need Distribution, not Development.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Why this happens more in CI than in Xcode
&lt;/h3&gt;

&lt;p&gt;Xcode's GUI does a lot of this matching for you automatically when "Automatically manage signing" is on. CI has none of that; it uses exactly the certificate and profile you hand it, which is more predictable once it's working but means every mismatch surfaces as a hard failure instead of getting quietly patched over.&lt;/p&gt;

&lt;p&gt;I hit every one of these building the CI pipeline behind &lt;a href="https://citolex.com" rel="noopener noreferrer"&gt;Citolex&lt;/a&gt;. Full writeup of the signing setup (and every other error this pipeline has produced) is at &lt;a href="https://macless.dev" rel="noopener noreferrer"&gt;macless.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>github</category>
      <category>cicd</category>
      <category>swift</category>
    </item>
    <item>
      <title>How to build and ship an iOS app without a Mac</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Sun, 16 Aug 2026 02:38:29 +0000</pubDate>
      <link>https://dev.to/maclessdev/how-to-build-and-ship-an-ios-app-without-a-mac-17o5</link>
      <guid>https://dev.to/maclessdev/how-to-build-and-ship-an-ios-app-without-a-mac-17o5</guid>
      <description>&lt;p&gt;If you want to put an app on the App Store, Apple requires you to build and sign it with Xcode, and Xcode only runs on macOS. That's true no matter what you built the app with — Swift, React Native, Capacitor, Flutter, whatever. At some point, something running on a Mac has to sign the thing.&lt;/p&gt;

&lt;p&gt;Not owning a Mac is a common enough problem that there are a few genuine ways around it, each with its own cost. Here's what I found when I hit this wall building an iOS app of my own, and what each option actually costs versus what it's usually advertised as.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Buy a Mac
&lt;/h3&gt;

&lt;p&gt;The straightforward option. A Mac mini starts around $799 (Apple raised the entry price in 2026), which is a one-time cost — no recurring bill, and it's hardware you own outright and can use for other things. If you're going to be doing iOS development regularly, long-term, this is genuinely the simplest answer and probably the right one.&lt;/p&gt;

&lt;p&gt;Where it doesn't make sense: a side project, a one-off app, or testing whether an idea is worth pursuing before you've made any money from it. Spending $799 to find out if your app idea has legs is a genuine barrier, and it's the reason a lot of iOS side projects never ship.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Rent a cloud Mac
&lt;/h3&gt;

&lt;p&gt;Services like MacStadium, MacinCloud, and similar exist specifically to rent you remote access to an actual Mac by the hour or month. Typical pricing runs anywhere from about $20/month for a shared, lower-spec machine up to $100+/month for dedicated, faster hardware.&lt;/p&gt;

&lt;p&gt;This solves the ownership problem but trades it for a recurring bill, and most of these services are built around interactive use — you remote into a desktop and click around, same as if you owned the machine. For something you might only need for a few minutes every time you cut a release, paying by the month is a lot of unused capacity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: GitHub Actions' free macOS runners
&lt;/h3&gt;

&lt;p&gt;The option that actually got me unblocked. GitHub Actions — GitHub's built-in CI/CD system — provides macOS build machines, and on public repositories, macOS runner minutes are free. They're intended for running test suites before a merge. Nothing about a macOS runner requires that it only run tests, though — it's an actual macOS machine with Xcode's command-line tools available, and it can run the exact same xcodebuild commands that sign, archive, and export an app that Xcode's GUI runs under the hood.&lt;/p&gt;

&lt;p&gt;At a high level, the pipeline looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git push origin main
→ GitHub spins up a free macOS build machine
→ workflow checks out your project
→ signing certificate + provisioning profile are installed from secrets you set once
→ xcodebuild archives the app and exports a signed .ipa
→ the .ipa uploads straight to App Store Connect / TestFlight
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Mac touched at any point. You push code, the machine that briefly exists to build it goes away when it's done, and the signed build shows up in TestFlight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it costs:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Ongoing?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Buy a Mac&lt;/td&gt;
&lt;td&gt;~$799+&lt;/td&gt;
&lt;td&gt;No — one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rent a cloud Mac&lt;/td&gt;
&lt;td&gt;~$20–100+/mo&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions (public repo)&lt;/td&gt;
&lt;td&gt;$0 for macOS build minutes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One thing to plan around: this $0 figure is specifically for public repos. Private repos work exactly the same way, pipeline and all — you just get a monthly allowance of free minutes instead of an unlimited amount, and pay per minute past it (macOS minutes run at roughly 10x the cost of standard Linux minutes). That's usually still cheaper than renting a Mac, just not literally free. Either way, your signing certificates and credentials sit in encrypted GitHub secrets, never exposed in the repo or build logs — repo visibility only affects CI minute pricing, nothing else. You also still owe Apple's $99/year Apple Developer Program fee regardless of which build option you use — none of this replaces that, it only removes the Mac requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where this approach actually gets hard
&lt;/h3&gt;

&lt;p&gt;The build pipeline itself is a fairly short workflow file. The part that actually eats time is code signing, because it's fussy in ways that aren't obvious until you've hit them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Certificate/profile mismatches.&lt;/strong&gt; A provisioning profile is generated against one specific signing certificate. If you regenerate the certificate in Apple's portal without regenerating every profile that references it, builds start failing with signing errors that don't clearly say why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact-name matching.&lt;/strong&gt; Provisioning profile names and bundle IDs have to match character-for-character (case and whitespace included) between Apple's portal and your CI configuration, or codesigning fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent TestFlight failures.&lt;/strong&gt; If you reupload a build using the same marketing version and build number as a previous upload, App Store Connect silently rejects it — but the upload tool reports success either way, so your CI log shows green while the build never actually appears in TestFlight. The fix is making sure your build number changes on every single run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export Compliance and App Privacy.&lt;/strong&gt; Not CI problems, but they'll block a TestFlight install or App Store submission if you haven't answered them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a sign the approach doesn't work — it's the same signing complexity every iOS build has, Xcode's GUI just hides more of it behind clicks. Once the certificates, profiles, and workflow are set up correctly once, every build after that is just git push.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which option is actually right for you
&lt;/h3&gt;

&lt;p&gt;If you're going to be doing iOS work regularly for years, buying a Mac is genuinely the simplest long-term answer. If you need occasional interactive access to a macOS desktop — for reasons beyond just building, like testing something Simulator-only — a rented cloud Mac covers that in a way CI can't. But if what you actually need is just: build this, sign it, get it into TestFlight, on a public repo — GitHub Actions' free macOS minutes get you there without spending anything on hardware or hosting.&lt;/p&gt;

&lt;p&gt;I went through all of this shipping &lt;a href="https://citolex.com" rel="noopener noreferrer"&gt;Citolex&lt;/a&gt;, a full App Store app with native Swift plugins, entirely through this pipeline. I packaged the GitHub Actions workflow, the signing script, and a setup guide covering every pitfall above as a one-time template at &lt;a href="https://macless.dev" rel="noopener noreferrer"&gt;macless.dev&lt;/a&gt; — no subscription.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>github</category>
      <category>cicd</category>
      <category>mobiledev</category>
    </item>
  </channel>
</rss>
