TL;DR

If iTunesConnect is rejecting your app with the message

This bundle is invalid - The file extension must be .zip.

then either you’ve got an incorrectly signed/malformed app or framework in your bundle, or it contains something that looks like one. It could even be a file name (not extension!) that ends in app, e.g. certificat.aveapp. In this case, simply remove or rename the file.

Looks like you’ve chosen the rambling, wall of text version! Welcome!

Instead of triumphantly submitting an app to never to think of it again, I spent the better part of a week staring at this error message from iTunes Connect:

This bundle is invalid - The file extension must be .zip.

What is wrong with the error message? It starts out so well: “THE bundle”. Definite article, so while there are many possible bundles in the universe, we’re clearly talking about a single such instance that you and I both know about. Unambiguous, great. The bundle. Apps are bundles*, so that must be the app. Too easy. Next!

The app is invalid! Silly me, despite the app passing Xcode’s validation, installing, codesigning & QAing just fine on several devices and OSes, the app is in fact bad. Makes sense, this is iTunesConnect, not some iPad2 out in the Styx! The stakes are higher and naturally so are the standards we are held to.

Onward. THE file extension must be zip. A-ha, another statement, juxtaposed, another definite article. There are many files and many extensions (possibly infinitely many! So exciting!) but THIS must be the one that you and I both know about. Hm. So that would make it the file extension of… the bundle? Which was .app. Well .ipa when exporting from the Xcode Organizer. But .ipa files are .zip files…

Odd, but I’m sure iTC is a hive of activity with the Apple Watch submissions. Maybe something is broken, just give the service its zipped app via Application Uploader and be on your way. Nope. This problem is stubborn.

When confronted with a stubborn problem, I like to try everything, but this time the qubits weren’t entangled enough and everything took linear time instead of constant time.

The Internet accused CocoaPods and unquoted spaces, CocoaPods and Swift and CocoaPods and frameworks.

The common theme here is CocoaPods, but the above issues all reduce to a space quoting bug in an old version of CocoaPods that was resulting in symlinks to frameworks instead of actual frameworks being deployed.

While the project did use CocoaPods, it did not use frameworks, swift (deploy target=5.1.1), there were no symlinks in the bundle and I had eliminated the space hypothesis early on. And due to a private pod repo in the project I couldn’t easily regenerate the project files with a newer version of CocoaPods. So if CocoaPods was to blame, the fault must lie somewhere in the project.

My systematic flailing was as follows:

Hmm, the 5.1.1 deployment target is awfully low. Let’s up that to 6, 7, 8. Nope.

Ah! This project has arm64, armv7s and armv7 all lumped in a 5.1.1 target. Maybe that’s not ok. Oops, it’s fine. Nope.

Silly! There is no arm64 slice at all and 64bit/iOS8 support is now a requirement! Maybe that’s what the message is trying to say! It would require a couple of new libraries and another unfortunate round of testing, but that’s plausible! Nope.

CocoaPods must be the killer! Let’s manually convert the cocoapods back into a traditional project! This was tiresome and didn’t actually work, but collapsing several projects with distinct build settings and phases into one was the first of several simplifying steps that allowed me to be a little more scientific in my flailing.

At this point, I started to suspect that either the iTunesConnect app or the account itself had become haunted. So I did what I should have done at the beginning: I made a new empty app with the same bundleID and submitted that. Then I added the required required 900 icon sizes, resubmitted and… it worked!

All I had to do then was incrementally change the failing app until it matched the passing one. The culprit was running out of places to hide. Every single build setting was made to match. Automatic module linking off? Suspicious as hell, turn it on. Google Analytics? Evil, kick it out.

In the end, all that was left in the scorched landscape was a strange certificate file for some webservice integration that had been only partially removed several months earlier. Its crime? A file extension of .aveapp which I guess looks enough like an app to kick off iTC’s recursive code signature checker. Infuriatingly, my eye had fallen on it several times during the ordeal. Its unashamedly non-X.509-ness and its disconcerting overlap with the apple developer tax of codesigning was a niggling question in my mind, but never would I have suspected that it could be mistaken for an app.

So it wasn’t CocoaPods’ fault at all, it was a combination of a sloppy project cleanup, a questionable definition of what constitutes an app on iTunesConnect’s part and a lazily worded error message.

I burnt through 34 minor version numbers and several days of my life to bring you this painfully earnt knowledge. But it didn’t need to be this way! Words are Important! They’re also cheap, maybe even free. Imagine how this might have gone if the iTC perl programmer (I assume my arch-nemesis loves perl) had written

This bundle is invalid - app file extensions must be .zip.

or even

This bundle is invalid - file “certificat.aveapp” not a valid app

* LOL that doco is out of date, it still claims iOS bundles don’t allow frameworks. Welcome to software engineering.