<?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>A green CI log and an empty TestFlight tab can both be telling the truth</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Tue, 15 Sep 2026 02:04:27 +0000</pubDate>
      <link>https://dev.to/maclessdev/a-green-ci-log-and-an-empty-testflight-tab-can-both-be-telling-the-truth-5e51</link>
      <guid>https://dev.to/maclessdev/a-green-ci-log-and-an-empty-testflight-tab-can-both-be-telling-the-truth-5e51</guid>
      <description>&lt;p&gt;Here's a failure mode with no error message at all: your pipeline finishes, every step is green, and TestFlight just... doesn't show a new build. Nothing crashed. Nothing timed out. Nothing logged a failure. You just refresh the page and there's nothing there.&lt;/p&gt;

&lt;p&gt;The reason is that App Store Connect's build processing happens in a completely separate, asynchronous system from whatever uploaded the binary. Your CI job's job ends the moment the upload call returns. Apple's own queue picks the binary up after that, and if it rejects it — a missing entitlement, a bad Info.plist value, whatever — that rejection happens entirely outside anything your pipeline was ever watching.&lt;/p&gt;

&lt;p&gt;This is the one case on my list where the actual fix isn't a script change, it's an admission: you don't have visibility by default, so you have to go add it. A step that polls App Store Connect's own build-status API after upload and asserts on the result is maybe ten lines of code, and it turns "silently missing, investigate later" into "failed at 2:14pm, here's why."&lt;/p&gt;

&lt;p&gt;I think about this every time I'm tempted to trust a tool's own exit code as the end of the story. The exit code tells you the command you ran finished. It says nothing about what happened to your output after it left your hands.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>cicd</category>
      <category>testflight</category>
      <category>appstoreconnect</category>
    </item>
    <item>
      <title>The three keychain errors that were all the same missing command</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Tue, 15 Sep 2026 02:03:44 +0000</pubDate>
      <link>https://dev.to/maclessdev/the-three-keychain-errors-that-were-all-the-same-missing-command-3e0h</link>
      <guid>https://dev.to/maclessdev/the-three-keychain-errors-that-were-all-the-same-missing-command-3e0h</guid>
      <description>&lt;p&gt;I lost the better part of a day to headless code signing, and the whole time I thought I was fighting three unrelated bugs.&lt;/p&gt;

&lt;p&gt;First, the build just hung — forever — at "[CP] Embed Pods Frameworks." No error, no timeout, just silence until the CI job itself timed out an hour later. Turns out &lt;code&gt;codesign&lt;/code&gt; was trying to pop an interactive "Allow keychain access?" dialog. There's no screen on a CI runner to click Allow on, so the process just waits for a click that will never come.&lt;/p&gt;

&lt;p&gt;Once I fixed that, I got "No certificate for team X" — even though I'd just imported the certificate two steps earlier, successfully, no errors. The cert was sitting right there in a temporary keychain. The problem was that nothing had told &lt;code&gt;codesign&lt;/code&gt; and &lt;code&gt;xcodebuild&lt;/code&gt; to actually look in that keychain — importing a cert and adding its keychain to the search list are two different steps, and I'd only done the first one.&lt;/p&gt;

&lt;p&gt;Fixed that too, and hit a third error: &lt;code&gt;SecItemCopyMatching: The specified item could not be found in the keychain&lt;/code&gt;, thrown by &lt;code&gt;security set-key-partition-list&lt;/code&gt;. The key import had worked. This command runs one step later, and it couldn't find the key to apply its ACL — because the password I'd passed to &lt;code&gt;-k&lt;/code&gt; didn't character-for-character match the password I'd used to unlock the keychain two lines above it in the same script. A typo in a variable name, effectively.&lt;/p&gt;

&lt;p&gt;Three different error messages, three different points in the script, and the root cause every time was the same category of mistake: something about the keychain wasn't fully "there" from the next command's point of view, even though it looked fine from mine. The fix for all three ended up being three lines, in this order, right after import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;security list-keychains &lt;span class="nt"&gt;-d&lt;/span&gt; user &lt;span class="nt"&gt;-s&lt;/span&gt; /tmp/my.keychain login.keychain
security set-keychain-settings &lt;span class="nt"&gt;-lut&lt;/span&gt; 3600 /tmp/my.keychain
&lt;span class="c"&gt;# (then, only after that): security set-key-partition-list -S apple-tool:,apple: -k "$SAME_PASSWORD" /tmp/my.keychain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're scripting headless signing for the first time, do all three up front. It'll save you the day I lost finding them one at a time.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>cicd</category>
      <category>xcode</category>
      <category>debugging</category>
    </item>
    <item>
      <title>TIL: a green checkmark in CI doesn't mean what you think it means</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Thu, 10 Sep 2026 01:28:49 +0000</pubDate>
      <link>https://dev.to/maclessdev/til-a-green-checkmark-in-ci-doesnt-mean-what-you-think-it-means-12f2</link>
      <guid>https://dev.to/maclessdev/til-a-green-checkmark-in-ci-doesnt-mean-what-you-think-it-means-12f2</guid>
      <description>&lt;p&gt;I spent an afternoon convinced our signing setup was broken because TestFlight never showed a new build. CI was green. Archive step: success. Export step: success. Upload step: success. Three green checkmarks and nothing on the other end.&lt;/p&gt;

&lt;p&gt;Turns out &lt;code&gt;xcodebuild&lt;/code&gt; and &lt;code&gt;altool&lt;/code&gt;/&lt;code&gt;xcrun notarytool&lt;/code&gt; can report a successful upload while Apple's backend rejects the binary asynchronously, after your CI job has already exited. The failure happens in App Store Connect's own processing queue, which your pipeline never checks and never will unless you add a step that polls for it.&lt;/p&gt;

&lt;p&gt;The fix is almost insultingly small: query App Store Connect's build status after upload instead of trusting the upload command's own exit code. But the real lesson generalizes past Apple's toolchain. A green checkmark only tells you the command you ran returned 0. It says nothing about whether the thing downstream that actually receives your output agreed to accept it. I've since started asking, for any pipeline step, "what's actually confirming success here, the tool I called or the system it called on my behalf?" Usually it's the former, and usually that's the gap.&lt;/p&gt;

&lt;p&gt;Anyone else have a CI success that turned out to be lying to a downstream system rather than to you directly?&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>testing</category>
      <category>devops</category>
      <category>ios</category>
    </item>
    <item>
      <title>Fix ITMS-90035: "Invalid Signature. A sealed resource is missing or invalid"</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Sun, 06 Sep 2026 20:29:43 +0000</pubDate>
      <link>https://dev.to/maclessdev/fix-itms-90035-invalid-signature-a-sealed-resource-is-missing-or-invalid-3jko</link>
      <guid>https://dev.to/maclessdev/fix-itms-90035-invalid-signature-a-sealed-resource-is-missing-or-invalid-3jko</guid>
      <description>&lt;p&gt;Upload finishes, then the rejection email arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR ITMS-90035: "Invalid Signature. A sealed resource is
missing or invalid. The file at path [MyApp.app/Frameworks/
SomeSDK.framework/SomeSDK] is not properly signed."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The headline sends people off to check their distribution certificate, regenerate a provisioning profile, and upload the same broken binary again. &lt;strong&gt;Your app signature is usually fine. Something inside the app bundle is not.&lt;/strong&gt; The bracketed path names it exactly, and that path is the only part of the message worth reading first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "sealed resource" means
&lt;/h2&gt;

&lt;p&gt;When you sign a bundle, codesign records a hash of everything inside it. That set of hashes is the seal. At upload, Apple recomputes it. If any file inside the bundle is missing, added, altered, or itself an unsigned piece of code, the recomputed seal no longer matches and the whole thing is rejected. So the error fires for two different situations that need opposite fixes: something inside was never signed, or something inside changed after signing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the path it named
&lt;/h2&gt;

&lt;p&gt;Unzip the exact &lt;code&gt;.ipa&lt;/code&gt; you uploaded and look at the offending item directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unzip &lt;span class="nt"&gt;-q&lt;/span&gt; MyApp.ipa &lt;span class="nt"&gt;-d&lt;/span&gt; out
codesign &lt;span class="nt"&gt;-dv&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 out/Payload/MyApp.app/Frameworks/SomeSDK.framework
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;code object is not signed at all&lt;/code&gt; means nothing signed it. An &lt;code&gt;Authority&lt;/code&gt; line naming a different team or a development certificate means it was signed, with the wrong identity.&lt;/p&gt;

&lt;p&gt;Then verify the whole bundle the way Apple does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codesign &lt;span class="nt"&gt;--verify&lt;/span&gt; &lt;span class="nt"&gt;--deep&lt;/span&gt; &lt;span class="nt"&gt;--strict&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 out/Payload/MyApp.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this on the archive before you upload, not after the rejection. It catches the same problem in a few seconds instead of an hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 1: a framework nothing is signing
&lt;/h2&gt;

&lt;p&gt;A third-party framework or &lt;code&gt;.dylib&lt;/code&gt; that gets copied into the bundle by a Copy Files build phase never gets signed, because copying is not embedding. Frameworks have to be listed under &lt;strong&gt;Embed &amp;amp; Sign&lt;/strong&gt; in the target's General tab. If a framework is set to Do Not Embed and a script drops it in anyway, the app signs cleanly and the framework inside it does not.&lt;/p&gt;

&lt;p&gt;The same applies to anything nested deeper. A binary framework that ships its own bundled dylibs can leave those unsigned even when the outer framework is embedded correctly, which is why the path in the error is sometimes several levels down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 2: something touched the bundle after signing
&lt;/h2&gt;

&lt;p&gt;This is the CI version, and it is almost always ordering. Signing happens during &lt;code&gt;xcodebuild -exportArchive&lt;/code&gt;. Any step that writes into the &lt;code&gt;.app&lt;/code&gt; after that point breaks the seal, even if the change looks harmless. Copying in a config file, swapping an icon, patching an &lt;code&gt;Info.plist&lt;/code&gt; value, rewriting a build number, all of it.&lt;/p&gt;

&lt;p&gt;The fix is order, not re-signing. Make the modification before the export step, so the signature is computed over the final contents. If a pipeline genuinely has to alter the app afterwards, it has to re-sign afterwards too, and that is a worse pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause 3: files that should not be in there
&lt;/h2&gt;

&lt;p&gt;Stray files inside the bundle can invalidate the seal on their own. Extended attributes and resource forks picked up from macOS are the common ones, along with &lt;code&gt;.DS_Store&lt;/code&gt; files that get swept in by an overly broad copy phase. Clear them before archiving:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xattr &lt;span class="nt"&gt;-cr&lt;/span&gt; path/to/MyApp.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also check that no build phase is copying an entire source directory into the bundle. If a copy phase uses a wildcard, it will happily include things you never meant to ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just re-sign with --deep" is bad advice
&lt;/h2&gt;

&lt;p&gt;It shows up in every thread about this error, and it does sometimes make the upload go through. Apple's own guidance is against it. &lt;code&gt;--deep&lt;/code&gt; re-signs everything inside the bundle with your identity, including code that was signed by someone else for a reason, and it papers over a build configuration that is still wrong. The next SDK update brings the error straight back. Fix the embed setting or the phase order instead.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>xcode</category>
      <category>appstore</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Fix "You uploaded an APK or Android App Bundle that was signed in debug mode"</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Sun, 06 Sep 2026 20:27:25 +0000</pubDate>
      <link>https://dev.to/maclessdev/fix-you-uploaded-an-apk-or-android-app-bundle-that-was-signed-in-debug-mode-5651</link>
      <guid>https://dev.to/maclessdev/fix-you-uploaded-an-apk-or-android-app-bundle-that-was-signed-in-debug-mode-5651</guid>
      <description>&lt;p&gt;The build succeeds. You drag the file into Play Console and get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You uploaded an APK or Android App Bundle that was signed in
debug mode. You need to sign your APK or Android App Bundle
in release mode.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The confusing part is that you did run a release build. &lt;strong&gt;Gradle does not fail when a release build has no signing config. It signs with the debug key instead.&lt;/strong&gt; Nothing in the build output flags it, so the first thing that notices is Play Console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirm it in ten seconds
&lt;/h2&gt;

&lt;p&gt;Do not guess at this. Read the signature off the artifact you actually uploaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;keytool &lt;span class="nt"&gt;-printcert&lt;/span&gt; &lt;span class="nt"&gt;-jarfile&lt;/span&gt; app-release.aab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the &lt;code&gt;Owner&lt;/code&gt; line. A debug-signed build says exactly this, on every machine in the world:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Owner: CN=Android Debug, O=Android, C=US
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see that, the diagnosis is done. If you see your own name or organisation instead, the upload was signed with a real key and your problem is a different one, usually the wrong key rather than a debug key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a release build uses the debug key
&lt;/h2&gt;

&lt;p&gt;Gradle picks the debug signing config for any build type that has no signing config of its own. That is a convenience for local debug builds and a trap for release builds. The three usual versions of it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No release signing config exists.&lt;/strong&gt; The &lt;code&gt;signingConfigs&lt;/code&gt; block was never added, or a &lt;code&gt;release&lt;/code&gt; config was added but never attached to the release build type. Defining it is not enough. The &lt;code&gt;buildTypes.release&lt;/code&gt; block has to reference it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The keystore file is not on the runner.&lt;/strong&gt; This is the CI-specific one. The config is correct, the &lt;code&gt;.jks&lt;/code&gt; is gitignored (correctly), and it was never written to the machine doing the build. Locally it works, in CI it silently produces a debug-signed bundle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You built the wrong task.&lt;/strong&gt; &lt;code&gt;assembleDebug&lt;/code&gt; and &lt;code&gt;bundleDebug&lt;/code&gt; produce debug artifacts by definition. Check that the task in your pipeline is &lt;code&gt;bundleRelease&lt;/code&gt;, and that you are uploading the file from &lt;code&gt;build/outputs/bundle/release/&lt;/code&gt;, not the debug folder next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Keep the passwords out of &lt;code&gt;build.gradle&lt;/code&gt;. Put them in a properties file that is gitignored, and read it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;keystorePropertiesFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rootProject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"keystore.properties"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;keystoreProperties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;keystoreProperties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;FileInputStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keystorePropertiesFile&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;android&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;signingConfigs&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;release&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;storeFile&lt;/span&gt; &lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keystoreProperties&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'storeFile'&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;
      &lt;span class="n"&gt;storePassword&lt;/span&gt; &lt;span class="n"&gt;keystoreProperties&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'storePassword'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;keyAlias&lt;/span&gt; &lt;span class="n"&gt;keystoreProperties&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'keyAlias'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;keyPassword&lt;/span&gt; &lt;span class="n"&gt;keystoreProperties&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'keyPassword'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;buildTypes&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;release&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;signingConfig&lt;/span&gt; &lt;span class="n"&gt;signingConfigs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;release&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In CI, base64 the keystore into a secret and write it back out before the Gradle step runs:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d &amp;gt; android/app/release.jks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Make it fail loudly next time
&lt;/h2&gt;

&lt;p&gt;The real problem here is the silence. A release build that quietly signs itself with a throwaway key is worse than one that stops. Add the guard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;keystorePropertiesFile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GradleException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"No keystore.properties. Release build would be debug-signed."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten seconds of setup, and the failure moves from Play Console back into the build log where it belongs.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing this is not
&lt;/h2&gt;

&lt;p&gt;Play App Signing does not remove the requirement. Google re-signs your app with the app signing key it holds, but you still sign the upload artifact yourself with your upload key, and that key can never be the debug key. The debug certificate is self-signed, generated automatically, and insecure by design. Play also requires an upload key valid past 22 October 2033, which the debug certificate will not satisfy in any case.&lt;/p&gt;

</description>
      <category>android</category>
      <category>googleplay</category>
      <category>gradle</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Debugging "Exporting for App Store Distribution failed"</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Sun, 30 Aug 2026 03:06:49 +0000</pubDate>
      <link>https://dev.to/maclessdev/debugging-exporting-for-app-store-distribution-failed-1ajd</link>
      <guid>https://dev.to/maclessdev/debugging-exporting-for-app-store-distribution-failed-1ajd</guid>
      <description>&lt;p&gt;If you've hit this exact banner, you already know it tells you almost nothing. "Exporting for App Store Distribution failed. Please download the logs artifact for more information" is the real wording from a live Apple Developer Forums thread, and that second sentence is the important one: the banner is a wrapper, not a diagnosis. Underneath it, Xcode is really just reporting that &lt;code&gt;xcodebuild -exportArchive&lt;/code&gt; exited with a non-zero status, most commonly exit code 70. What actually caused that exit code is a completely different question every time, and it's sitting in the export logs, not in the banner text.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this error actually is
&lt;/h2&gt;

&lt;p&gt;Xcode Cloud shows this exact banner, and Xcode's own Distribute App flow in Organizer shows a version of the same thing, whenever the archive-to-.ipa export step fails for any underlying reason. It's the export equivalent of a generic "something went wrong" — genuinely useful only as a signal to go open the logs, not as a description of the problem itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real causes, pulled from real threads
&lt;/h2&gt;

&lt;p&gt;These aren't guesses. Each of these is a genuinely diagnosed case from Apple's own Developer Forums or fastlane's public issue tracker, not a paraphrase of "common wisdom."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No valid distribution certificate.&lt;/strong&gt; The export fails with "No signing certificate 'iOS Distribution' found" when the cert Xcode expects is missing, expired, or revoked. In one Xcode Cloud case, the fix was revoking the Xcode-Cloud-managed "Distribution Managed" certificate to force it to regenerate a valid one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transient Apple backend failures.&lt;/strong&gt; Sometimes the underlying error really is "Communication with Apple failed" (a 502) or a backend authentication-context bug, with nothing wrong in your project at all. There's no project-side fix for these — they resolve when Apple's own systems recover, or need an actual support ticket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An entitlement that doesn't match the provisioning profile.&lt;/strong&gt; One real case traced back to a typo in an entitlement key, which made the profile fail Apple's qualification check on export even though everything looked fine in Xcode's UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A shadowed system tool in your build environment.&lt;/strong&gt; Homebrew's &lt;code&gt;rsync&lt;/code&gt; can shadow macOS's own &lt;code&gt;rsync&lt;/code&gt; in &lt;code&gt;PATH&lt;/code&gt;, breaking the file copy step during export with the same generic exit-70 failure — a build-environment bug that has nothing to do with signing at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An Xcode version regression.&lt;/strong&gt; Xcode 26.2 shipped an invalid App Store Connect API parameter for projects with a watchOS target, which broke automatic signing during export. Apple confirmed it as a real bug; the workaround was reverting to Xcode 26.1 until it's fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed recently
&lt;/h2&gt;

&lt;p&gt;Two dated platform changes are worth knowing about, even though neither has a confirmed direct link to this specific error banner — they're real, recent changes to the surrounding signing and distribution landscape:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy manifests, enforced since May 1, 2024.&lt;/strong&gt; Newly added third-party SDKs on Apple's commonly-used-SDK list now require a privacy manifest. This is a separate, earlier gate than the export step, but it's part of the same broader "your binary has to check out before Apple will accept it" surface that's expanded in the last two years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Xcode 26.2's export regression, August 2026.&lt;/strong&gt; The watchOS-target signing bug above is dated and Apple-acknowledged (filed as FB21322904 and FB21627607) — worth checking if you're on that exact Xcode version and your project has a watch target.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually helps
&lt;/h2&gt;

&lt;p&gt;The single most useful habit here is treating the banner as a prompt to go looking, not as the answer itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open the actual export logs, not the summary banner.&lt;/strong&gt; Xcode Cloud's logs artifact and Xcode Organizer's own distribution logs both contain the real underlying error. The banner text is identical across every one of the causes above — the logs are where they stop looking identical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify your certificate and profile directly in the portals.&lt;/strong&gt; Don't trust what Xcode's local cache shows. Check the actual expiration and status of your distribution certificate and provisioning profile in Apple Developer and App Store Connect, since a locally-cached profile can look fine in Xcode while being invalid server-side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check every extension target has its own App ID and profile.&lt;/strong&gt; Widgets, share extensions, and Safari Web Extensions each need their own explicit App ID and provisioning profile — a mismatch or missing profile on just the extension target, while the main app target is fine, is a real and recurring cause of export failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clean derived data and re-download profiles before assuming it's a code problem.&lt;/strong&gt; A stale local cache of a since-revoked or since-renewed certificate or profile causes failures that have nothing to do with your actual project configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If nothing project-side is wrong, consider that it might just be Apple's backend.&lt;/strong&gt; Some of these failures are genuinely transient server-side issues. If the logs point at a communication or authentication error rather than a signing mismatch, retrying later is a legitimate first move, not giving up.&lt;/p&gt;

&lt;p&gt;The pattern underneath all of this: the banner text is the same no matter which of these five things actually happened. Every real fix starts with going past it into the logs, not with guessing based on the banner alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  I built a free tool for this
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://macless.dev/signing-doctor.html" rel="noopener noreferrer"&gt;Signing Doctor&lt;/a&gt; (free) specifically because this class of error is so generic on its face and so specific underneath. Paste in your actual error output and it helps you figure out which of the real underlying causes you're actually looking at, rather than guessing from the banner text alone. It's part of the same project as &lt;a href="https://macless.dev/#pricing" rel="noopener noreferrer"&gt;Macless&lt;/a&gt;, which handles the signing and CI side of shipping an iOS app to TestFlight without a Mac.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>xcode</category>
      <category>debugging</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>The most common reasons Apple rejects your app</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Sun, 30 Aug 2026 00:00:09 +0000</pubDate>
      <link>https://dev.to/maclessdev/the-most-common-reasons-apple-rejects-your-app-20gl</link>
      <guid>https://dev.to/maclessdev/the-most-common-reasons-apple-rejects-your-app-20gl</guid>
      <description>&lt;p&gt;Getting a rejection email from App Review feels personal. It usually isn't. Apple runs the same review process against every submission, and most rejections trace back to a small number of guidelines that come up again and again. Knowing which ones, and what they actually mean, turns a vague rejection into a fixable checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  How often this actually happens
&lt;/h2&gt;

&lt;p&gt;Apple's own 2024 App Store Transparency Report puts real numbers on this. Out of 7.77 million app submissions reviewed that year, 1.93 million were rejected, roughly 25%. Of those, 295,109 were fixed and approved on resubmission. Separately, 82,509 already-live apps were removed after the fact, most commonly for guideline or design violations (42,252), followed by fraud (38,315).&lt;/p&gt;

&lt;p&gt;Apple hasn't published a breakdown of rejections by specific guideline number, so treat any listicle claiming "62% of rejections are X" as unsourced. What Apple has said, in its own commentary alongside the report, is that the most common drivers, in order, are performance and bugs, legal issues, design problems, business-model (payment) violations, and safety risks. That ordering lines up with the specific guidelines below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The guidelines that actually catch people
&lt;/h2&gt;

&lt;p&gt;These are pulled directly from Apple's current App Store Review Guidelines, not paraphrased from a third party.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guideline 2.1, App Completeness.&lt;/strong&gt; Covers crashes, obvious bugs, placeholder content, broken demo accounts, and non-functional in-app purchases. If a reviewer can't get past your login screen or your app crashes on launch, this is the line it gets cited under. It's the single most avoidable category, because it's the one you can actually test yourself before submitting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guideline 4.2, Minimum Functionality.&lt;/strong&gt; Your app has to be more than "a repackaged website." Apple wants "lasting entertainment value or adequate utility." A thin wrapper around a web view, with no native functionality added, gets flagged here. A sub-clause, 4.2.6, specifically targets apps built from commercialized app-generator templates unless the actual content provider is the one submitting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guideline 4.3, Spam.&lt;/strong&gt; 4.3(a) bans duplicate submissions under different Bundle IDs, the classic example being the same app re-skinned per city. 4.3(b) targets near-clones in saturated categories (flashlights, simple timers, wallpaper apps, fortune-telling apps) unless yours is "meaningfully different" from what's already in the store.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guideline 5.1.1, Data Collection and Storage.&lt;/strong&gt; Requires a working privacy policy link, explicit consent before collecting any data (including "anonymous" analytics), no forcing or tricking users into granting permissions they don't need, and account deletion if your app lets people create an account in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guideline 3.1.1, In-App Purchase.&lt;/strong&gt; Any unlock of digital features or content inside your app has to go through Apple's own In-App Purchase system. License keys, QR codes, or crypto-based workarounds to unlock paid features get rejected here, and loot-box mechanics specifically require published odds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed recently
&lt;/h2&gt;

&lt;p&gt;Two real, dated changes worth knowing about if you haven't submitted in a while:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy manifests, enforced since May 1, 2024.&lt;/strong&gt; Newly added third-party SDKs on Apple's commonly-used-SDK list now require a privacy manifest and, in many cases, a valid signature. An app missing one for a flagged SDK gets rejected before a human reviewer even looks at it. Apple has signaled this requirement will keep expanding to cover more of the binary, not less.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guideline 1.2 now explicitly covers random or anonymous chat, as of February 2026.&lt;/strong&gt; If your app includes any kind of stranger-matching chat feature, Apple's guidelines were revised to state plainly that this falls under User-Generated Content rules, meaning you need real moderation in place, not just a report button as an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually helps
&lt;/h2&gt;

&lt;p&gt;None of this is exotic. Most of it is just discipline about what you check before you hit submit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the actual rejection line, not just the guideline number.&lt;/strong&gt; App Review's message usually tells you specifically what triggered it, a missing purpose string, a specific screen that crashed, a mismatched screenshot. The guideline number is the category, not the diagnosis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Give reviewers a working demo account.&lt;/strong&gt; If your app requires login, App Review needs a real account that actually works, with real data behind it, not a stub. A broken demo account is one of the most common and entirely self-inflicted 2.1 rejections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't build a payment workaround.&lt;/strong&gt; If a feature unlocks with money, it has to go through Apple's In-App Purchase system. This is 3.1.1 and Apple checks for it specifically, including license keys and external payment links for digital content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check your privacy manifest before you add a new SDK, not after a rejection.&lt;/strong&gt; If you're pulling in a new third-party dependency, check whether it's on Apple's commonly-used-SDK list and whether it ships a manifest. Finding this out from a rejection costs you a review cycle you didn't need to spend.&lt;/p&gt;

&lt;p&gt;The pattern underneath all five categories: Apple is checking whether your app does what it claims, respects the data it touches, and pays through the channel it's supposed to. Most rejections are one of those three things, stated in guideline language instead of plain English.&lt;/p&gt;

&lt;h2&gt;
  
  
  I built a free tool for this
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://macless.dev/rejection-doctor.html" rel="noopener noreferrer"&gt;Rejection Doctor&lt;/a&gt; (free) after seeing how often the same handful of guidelines come up. Paste in your actual rejection message and it matches it against 57 real rejection categories, tells you what's actually going on, and can draft an appeal letter if you think the rejection was a mistake. It's part of the same project as &lt;a href="https://macless.dev/#pricing" rel="noopener noreferrer"&gt;Macless&lt;/a&gt;, which handles the signing and CI side of shipping an iOS app without a Mac.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>appstore</category>
      <category>mobiledev</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>The iOS certificate problem nobody has actually solved</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Sat, 29 Aug 2026 21:38:16 +0000</pubDate>
      <link>https://dev.to/maclessdev/the-ios-certificate-problem-nobody-has-actually-solved-1gff</link>
      <guid>https://dev.to/maclessdev/the-ios-certificate-problem-nobody-has-actually-solved-1gff</guid>
      <description>&lt;p&gt;If you have ever set up iOS code signing for a CI pipeline, you already know the feeling. Everything works for months, then one day a build fails with a certificate error, and you spend the next two hours relearning a system you thought you understood.&lt;/p&gt;

&lt;p&gt;I want to write about why that keeps happening, because it is not a "you set it up wrong" problem. It is a problem the most popular tool in this space has openly said it has not solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The admission
&lt;/h2&gt;

&lt;p&gt;fastlane is the standard way most teams manage iOS signing. It is free, open source, and has been around since 2015. Its own maintainers have said, on a public GitHub discussion, that certificate lifecycle management is one of the few things fastlane still has not figured out:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The problem of expiring certificates to me is one of the few fundamental signing features that fastlane has not yet solved... we all face again and again... I really hope we can put this issue to bed soon!"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is not a random forum comment. That is a maintainer, on the project's own GitHub, describing a problem that keeps coming back for everyone who uses the tool. If you search fastlane's issue tracker, you will find real, specific reports of what that looks like in practice. One developer described &lt;code&gt;fastlane match&lt;/code&gt; hanging indefinitely on "Installing provisioning profile," and said outright: "I accidentally tried waiting for 2.5 hours." Another reported an expired WWDR intermediate certificate silently breaking their local keychain with no clear error pointing at the actual cause.&lt;/p&gt;

&lt;p&gt;And when match genuinely gets stuck, the documented recovery path is &lt;code&gt;match nuke&lt;/code&gt;, which deletes every certificate and profile of the affected type for your entire team, not just yours. One developer's reaction to that in the same thread sums it up: "does nuking remove all certificates &amp;amp; profiles... including other people's... in your iOS team?" Yes. It does.&lt;/p&gt;

&lt;p&gt;I am not writing this to pick on fastlane. It is a genuinely useful tool and a lot of serious iOS shops run on it successfully. I am writing this because if the tool built specifically to solve this problem says the problem is not solved, that tells you something real about how the underlying system works, not about who is using it wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the certificates themselves are the trap
&lt;/h2&gt;

&lt;p&gt;Apple caps how many active Distribution certificates you can have on an account. In practice this is usually two or three. That sounds generous until you see what actually exhausts it.&lt;/p&gt;

&lt;p&gt;If your CI pipeline is set up to auto-provision (generate a fresh certificate on every run instead of reusing one you already created), you will hit that cap surprisingly fast. Bitrise's own documentation for their automatic provisioning steps says this plainly: without a certificate you pre-uploaded yourself, "steps with automatic provisioning will generate one on the fly every time." Run that pipeline a few dozen times and you can burn through your entire certificate allowance without ever manually creating one.&lt;/p&gt;

&lt;p&gt;When you do hit the cap, the error from Apple's own API is not exactly friendly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Could not create another Distribution certificate,
reached the maximum number of available Distribution certificates."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That message tells you what happened. It does not tell you which of your existing certificates is safe to revoke, or which apps depend on which certificate. If you have more than one app signed with certificates from the same account, you now have to go figure that out manually before you can unblock yourself, usually while a release is waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The errors that are not your fault
&lt;/h2&gt;

&lt;p&gt;A few other things I have hit, or seen well documented elsewhere, that are worth knowing before they happen to you at 11pm the night before a submission:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A bare 500 on profile creation.&lt;/strong&gt; &lt;code&gt;UNEXPECTED_ERROR&lt;/code&gt; on a &lt;code&gt;POST&lt;/code&gt; to the provisioning profiles endpoint is a real, recurring error reported directly on Apple's own developer forums. Sometimes it resolves on a plain retry with zero changes to your config. If you see this, try again before you start second-guessing your setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The wrong kind of API key.&lt;/strong&gt; App Store Connect API keys come in two flavors: Individual and Team. Provisioning-related endpoints specifically require a Team key. If you are using an Individual key, you will get a 403 on exactly those endpoints while everything else you try with that same key works fine, which makes it a lot more confusing to diagnose than a blanket permission error would be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise vs. App Store account type is invisible to the API.&lt;/strong&gt; Apple's own documentation admits the API cannot automatically tell these two account types apart, so if you are generating certificates programmatically for an Enterprise (in-house distribution) account, you need to set the &lt;code&gt;in_house&lt;/code&gt; flag manually. Nothing will warn you if you get this wrong until later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apple can change the API out from under you with no warning.&lt;/strong&gt; In 2025, Apple silently removed an undocumented parameter, &lt;code&gt;templateName&lt;/code&gt;, from the App Store Connect API. That parameter was load-bearing for fastlane's automated provisioning-profile creation, and its removal broke automated signing industry-wide with no migration path and no changelog entry anyone could point to in advance. There is a real postmortem about it if you want the full story (search "Sourcetoad Apple templateName fastlane"), and the related GitHub issue was still open the last time I checked. This is the part that is genuinely hard to defend against: the tool can be correct today and wrong next month because Apple changed something nobody announced.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually helps
&lt;/h2&gt;

&lt;p&gt;None of this means give up and do everything by hand. A few things that make a real, measurable difference:&lt;/p&gt;

&lt;p&gt;Check your certificate count before you create a new one, not after you get the error. A simple &lt;code&gt;GET&lt;/code&gt; on the certificates endpoint tells you exactly how many of your two or three slots are already in use, so you can decide whether to reuse one or revoke an old one before you are blocked mid-release.&lt;/p&gt;

&lt;p&gt;Never auto-provision on every CI run. Generate your signing certificate and provisioning profile once, store them (encrypted) somewhere your pipeline can reach, and reuse them. This alone avoids most of the cap-exhaustion problem entirely.&lt;/p&gt;

&lt;p&gt;Build retry logic around the specific error codes above, not a generic catch-all. A 500 on profile creation deserves a retry with backoff. A 403 on provisioning specifically, when your key works everywhere else, deserves a message that says "check whether this is a Team key," not "permission denied."&lt;/p&gt;

&lt;p&gt;Keep a place to watch for expiry before it becomes an outage. A scheduled check that looks at your certificate and profile expiration dates and pings you a few weeks out is a small thing to build and it turns a surprise failure into a calendar reminder.&lt;/p&gt;

&lt;p&gt;I ended up building most of the last two points directly into my own pipeline (Macless, macless.dev), because I kept hitting exactly this set of problems while shipping my own app without owning a Mac, and got tired of relearning the same lessons every few months. If you are dealing with this on your own setup right now, I hope at least the specific error messages above save you the hour I spent figuring out what they meant the first time.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>githubactions</category>
      <category>mobile</category>
      <category>cicd</category>
    </item>
    <item>
      <title>I hosted the no-Mac iOS pipeline</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Thu, 27 Aug 2026 05:01:48 +0000</pubDate>
      <link>https://dev.to/maclessdev/i-hosted-the-no-mac-ios-pipeline-its-1-today-53i2</link>
      <guid>https://dev.to/maclessdev/i-hosted-the-no-mac-ios-pipeline-its-1-today-53i2</guid>
      <description>&lt;p&gt;A bit ago I wrote about the GitHub Actions setup I built to ship Citolex to the App Store without owning a Mac. That part hasn't really changed. What changed is how you actually set it up.&lt;/p&gt;

&lt;p&gt;Before this, you'd download a template and run a script yourself. Works fine, but it's still a terminal and a bunch of manual steps between paying and actually having something running.&lt;/p&gt;

&lt;p&gt;So I hosted it. You pay, log into GitHub, pick or make a repo, and a wizard pushes the whole pipeline into it for you using GitHub's API, you don't touch git at all. Then it walks you through your signing secrets and actually tells you what's wrong with a cert or profile before you waste a build on it, not after.&lt;/p&gt;

&lt;p&gt;The signing check part runs as a Cloudflare Worker, no Mac or VM behind it. Turns out provisioning profiles and certs are just regular crypto formats, Web Crypto can read them fine on its own.&lt;/p&gt;

&lt;p&gt;It's $19.99/month, or $299 once and nothing after — iOS and Android both. If you've fought with iOS signing in CI before, I'd like your eyes on it.&lt;/p&gt;

&lt;p&gt;I built this, it's my thing. macless.dev&lt;/p&gt;

</description>
      <category>ios</category>
      <category>githubactions</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Citolex is live on the App Store, built without owning a Mac</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Mon, 24 Aug 2026 22:40:03 +0000</pubDate>
      <link>https://dev.to/maclessdev/citolex-is-live-on-the-app-store-built-without-owning-a-mac-39fi</link>
      <guid>https://dev.to/maclessdev/citolex-is-live-on-the-app-store-built-without-owning-a-mac-39fi</guid>
      <description>&lt;p&gt;Citolex just went live on the App Store. It's a speed-reading app, flash-based word-by-word reading with synced text-to-speech, everything on-device.&lt;/p&gt;

&lt;p&gt;The part I actually want to write about is how it got built and shipped. I don't own a Mac. Xcode needs one to sign and archive an iOS build, so instead of buying hardware I built a GitHub Actions pipeline that does it for me.&lt;/p&gt;

&lt;p&gt;Capacitor wraps the web app (HTML/JS/CSS) in a native iOS shell, so most of the UI didn't need native code. But Citolex needed a few things Capacitor can't reach on its own: text-to-speech, a Share Extension so you can send text into the app from Safari or Notes, an App Group so the extension and the main app can share data, and background audio so narration keeps playing when the app isn't in the foreground. Those are native Swift plugins layered on top of the Capacitor shell.&lt;/p&gt;

&lt;p&gt;The workflow runs on GitHub's free macOS build minutes (available on public repos), checks out the code, pulls signing certs and provisioning profiles from GitHub secrets, archives with xcodebuild, exports a signed ipa, and uploads to App Store Connect on a normal git push.&lt;/p&gt;

&lt;p&gt;What actually took the time wasn't the workflow file. It was the signing debug loop: certificate and profile mismatches, needing the exact same name between the Apple Developer portal and the CI config, and TestFlight silently rejecting a reupload with an unchanged build number while the CI log stayed green the whole time with no useful error. Eventually I added a validate step that runs before the real build and checks the Team ID and bundle ID actually match what's in the provisioning profile, so a mismatch fails fast with a specific message instead of a cryptic xcodebuild error twenty minutes in.&lt;/p&gt;

&lt;p&gt;It shipped. Citolex is live, built and signed entirely through that pipeline, no Mac at any point: apps.apple.com/us/app/citolex/id6800933053&lt;/p&gt;

&lt;p&gt;If you're in the same position, no Mac, need to ship a real iOS app, that pipeline is now packaged up as Macless (macless.dev).&lt;/p&gt;

</description>
      <category>ios</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Orchard Cloud's Pricing Philosophy, Explained</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Sun, 23 Aug 2026 17:37:08 +0000</pubDate>
      <link>https://dev.to/maclessdev/orchard-clouds-pricing-philosophy-explained-5cel</link>
      <guid>https://dev.to/maclessdev/orchard-clouds-pricing-philosophy-explained-5cel</guid>
      <description>&lt;p&gt;A lot of people ask us why Orchard Cloud costs what it costs. Fair question. Here's the honest answer.&lt;/p&gt;

&lt;p&gt;We looked at what enterprise cloud compute costs, we looked at what enterprise device leasing costs, and then we asked what it would take to combine the two into a single line item a finance team could approve without too many follow-up questions.&lt;/p&gt;

&lt;p&gt;Starter exists mostly so people have something to compare Growth to. This is a well-understood pattern and we're not going to pretend otherwise.&lt;/p&gt;

&lt;p&gt;Enterprise is "Contact us" because at that volume we genuinely do need to have a conversation, mostly about logistics.&lt;/p&gt;

&lt;p&gt;Onboarding takes 12 to 16 months. We used to say 6 to 8 weeks. We've since gotten more experience with what onboarding actually involves, and the current estimate reflects that.&lt;/p&gt;

&lt;p&gt;If any of this makes sense to you, &lt;a href="https://orchard.macless.dev" rel="noopener noreferrer"&gt;orchard.macless.dev&lt;/a&gt; has a calculator.&lt;/p&gt;

</description>
      <category>satire</category>
      <category>startup</category>
    </item>
    <item>
      <title>How We Built Orchard Cloud's Enterprise Mac Provisioning Platform</title>
      <dc:creator>Jackson</dc:creator>
      <pubDate>Sun, 23 Aug 2026 16:09:44 +0000</pubDate>
      <link>https://dev.to/maclessdev/how-we-built-orchard-clouds-enterprise-mac-provisioning-platform-1c3n</link>
      <guid>https://dev.to/maclessdev/how-we-built-orchard-clouds-enterprise-mac-provisioning-platform-1c3n</guid>
      <description>&lt;p&gt;At Orchard Cloud, we give engineering orgs elastic, on-demand access to fruit-branded computers they'll never personally own. Here's a look at how the platform works under the hood.&lt;/p&gt;

&lt;p&gt;Provisioning runs through a queue, evaluated against fleet availability and priority tier. Once approved, you get remote access to a Mac located somewhere in our fleet — we don't say where, for reasons of security.&lt;/p&gt;

&lt;p&gt;We also built a Total Cost of Mac Rental calculator so customers can see exactly what indefinite rental costs against just buying a Mac outright. It's the most honest part of the platform, which should tell you something.&lt;/p&gt;

&lt;p&gt;Full pricing, onboarding timelines, and the provisioning flow are live now.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://orchard.macless.dev" rel="noopener noreferrer"&gt;https://orchard.macless.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>satire</category>
      <category>saas</category>
      <category>humor</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
