I made some changes to the repo of our private pod. Pushed up my branch. Tests all ran. Then my colleague asked for some updates to my PR (pull request). I made changes and pushed it to GH. I wanted to merge the changes. But then CI was failing. So I ran CI again. It failed again. They say third time is the charm. So I ran it two more times. It still failed. I thought I had changed something in the project file, because I was getting some Swift compatibility error. I erased the derived data and clean build. I was able to build and run all tests. I wasn’t sure why things were still failing.
I tried creating a new dummy PR with just an empty new file. It still failed. I then thought it was perhaps something in the last commit. It wasn’t.
Root Cause
Problem was from an incompatible dependency. But how did it show up all of a sudden out of no where?!
The PodSpec
was doing something like:
s.dependency 'FoodFire', '~> 4.0'
Note: ThePodfile
didn’t have any mention of FoodFire
. It just relied on the PodSpec
for its dependencies.
A single CI run does two things:
- Build, compile the project. Go through its unit-tests and UI-tests.
- Do
pod lib lint --private --allow-warnings --verbose --fail-fast --skip-test
So lets say the last time you did pod update FoodFire
the latest release of FoodFire
was 4.1.3
. This will write the following into your Podfile.lock
:
PODS:
- APIClient (1.4.0)
- FoodFire (4.1.3): # This is the restriction you ultimately end up with.
- Trucks (~> 7.7.1)
- MyCoolPod (24.0.0): # THIS IS OUR POD
- SmoothManager (= 2.0.1)
- APIClient (~> 1)
- SwiftyGif (= 9.0.1)
- FoodFire (~> 4.0) # This the restriction set on the version.
- SmoothManager (2.0.1)
- SwiftyGif (4.2.1)
- Trucks (7.7.1)
In your CI, you run the Example app and all its unit-tests + UI-tests. Great. Everything works.
Then you do pod lib lint
and all of sudden things go to hell 🤬.
The following will explain how that can happen.
Podfile.lock
is usingFoodFire
4.1.3- An engineer goes in and publishes new 4.1.4
FoodFire
PodSpec. - Your CI then goes through its linting process:
- run
pod repo update
so your agent has all the latest resources it needs - run
pod lib lint
- run
What you have to understand is that:
- At step 3.1, your agent has now downloaded the 4.1.4 PodSpec.
- Your linting is rightfully oblivious to
Podfile.lock
. It will use 4.1.4 (not 4.1.3)
At this moment your linting vs. normal project building/running have bifurcated into using two different sets of dependencies.
In my situation, the 4.1.4 was causing a compilation error. Linting was failing.
Debugging tips to resolve such issues faster?
- In general if things only fail on CI, then it’s usually a good idea to go step by step with exactly what your CI is doing.
- Run
pod lib lint
locally and see if things work. If doesn’t then look into the logs for anomaly. See if all the dependency is using the version you’re expecting. - Pay closer attention to the version thats logged in CI. See if it’s the version you expect.
- Run the app locally with the version CI gives and see if things work as expected.
Summary of Problem
pod lib lint
is oblivious to Podfile
& Podfile.lock
. It only cares about the versions mentioned in the PodSpec
.
For this reason, if you’re using the optimistic operator then the outcome of
pod repo update
pod lib lint
will vary depending on the time it was ran.
In general stuff you do in CI, dependency management, build phase scripts are things that go unnoticed when developing or reviewing code. Having an insight to look into these can be helpful.
Proposal Discussion:
I explained the problem to some CocoaPods experts and mentioned my proposal. The following is that conversation [with some edits]:
In your CI before doing anything run: pod update <FooPod>
— where: the PodSpec is specifying the version using the optimistic operator (~>
). This allows your project’s UI, Unit-tests run with the latest possible dependencies.
Orta one of CocoaPods original creators replied back with:
yep, for the first few years we recommended adding the
podfile.lock
to.gitignore
if you were shipping a library which is effectively the same thing.But too many people misinterpreted it
…
in the typescript compiler we don’t have a lockfile for example
Interesting. And just to be sure. Also ignore /Pods
. Right?
yes, I normally always do this. but I understand the desire.
My altered Proposal:
- Keep the
Podfile.lock
+/Pods
folder. - Ask devs to do
pod update
more aggressively. You could just add this as part of your build phase, pre-commit hook etc.- Add a
pod update
step in our CI — before you build the Example app and run your tests.
- Add a
- For
pod update
to work as expected:- Avoid specifying any dependency in your
Podfile
— if it’s already specified in yourPodSpec
. Otherwise your example is restricting what you’ll test.
- Avoid specifying any dependency in your
- Note: In the case that you pass
--skip-tests
to yourpod lib lint
command, the linting is still needed. It checks two things:- If the
PodSpec
is valid. - If app compiles using the latest dependencies specified in the
PodSpec
. Since this ignores the Podfile, it will act as aggressively as possibly. Which is good. It helps you catch any breaking change that wasn’t versioned correctly.
- If the
Final Summary
The manner in which you maintain dependencies for a library as an owner is to be different from how you maintain dependencies for a project/app/library as a consumer.
As library owners we have very little control over what the host app does for the dependencies we’ve listed in our PodSpec
. We can’t tell if it’s using the minimum specified or the maximum specified.
For this reason, it’s always great to not stay behind your dependencies i.e. you don’t want your example app to be locked to 4.1 of your dependency while your actual host app is using the 4.9 version of your dependency.
Technically speaking 🤞if all your dependencies use Semantic versioning correctly🤞 then you shouldn’t run into problems because of lagging behind a couple versions.
However if you’re more aggressive with building your app with the latest dependencies, you can provide feedback faster to the maintainer of your dependency. Example of feedback are:
- App doesn’t compile. You didn’t do semantic versioning correctly.
- App compiles, but behavior is different:
- Pod still returns an image, but it’s a totally different image and you weren’t expecting this
- The function syntax is the same, however it’s three times slower.
- The function syntax is the same, however the results are different. Jordan Rose, an Apple Engineer mentions a subtle note on semantic versioning on Binary Frameworks in Swift - 21:33 from the session.
I’ve added a new private property to the Spaceship class. And I’m using it in the Spaceship’s initializer.
Now, neither of these things are going to appear in the module interface. They’re not part of your framework’s public API.
So this sort of change only requires updating the minor, or the Patch Version component.
Keep in mind though that I did change the behavior of the initializer, and so if this was documented behavior before, then this would be a semantics breaking change, and clients would have to consider whether to update, and therefore, I should change the major version number instead.
Acknowledgements
I like to thank all the CocoaPod creators and contributors. Also give special thanks to Orta for sharing his insight and the historical context on maintaining a library.