I have a few private Cocoapods that I use across multiple projects and I use Fastlane to help automate the release of new versions.
Example Cocoapod: PodRepo
For this post I’m going to use an imaginary Cocoapod called "PodRepo", which repos your pod in a pleasant and satisfying way. Here’s what the root of the directory structure might look like:
Gemfile Gemfile.lock fastlane/ PodRepo/ LICENSE README.md PodRepo.xcodeproj PodRepo.podspec
Sharing Fastlane Lanes
All of my Cocoapod projects use the same, global Fastlane file.
I do not consider this a best practice, especially if you’re working with other people, as it means that the lane actions are not part of the repo. As a solo dev, however, it works for me because it allows me to share common lanes across multiple Cocoapod projects.
My Fastlane actions for updating a Cocoapod can be summarised like this:
- Run any tests that the Cocoapod might have
- Run
pod lib lint
to ensure the pod validates - If they pass, bump the version number in the
.podspec
file based on the update type (i.e.major
,minor
, orpatch
) and store this new version number in a variable - Commit this change to the
.podspec
file - Bump the project version using the podspec variable, and bump the version of all of the targets
- Commit these changes to the project file and the
Info.plist
files of the targets - Notify me that it’s completed via Slack
The local Fastfile declares variables that will be used by the globally shared Fastfile. The $workspace
, $scheme
, $spec
and $project
are in each individual project.
So for my PodRepo example, it would have a fastlane/Fastfile
that would look like this:
$scheme = "PodRepo" $podspec = "/PodRepo.podspec" $project = './PodRepo.xcodeproj' import "../../Fastlane/FastfilePods"
The Global Fastfile
That import statement imports the global FastfilePods
Fastfile and makes its lanes available to the project:
before_all do |lane, options| ENV["SLACK_URL"] = "<SLACK HOOK URL>" if options[:skip_checks] UI.message "Skipping git status checks" else ensure_git_status_clean end end desc "This does the following: " desc "" desc "- Runs the unit tests" desc "- Ensures Cocoapods compatibility" desc "- Bumps the patch version" lane :patch do update(type: "patch") end desc "This does the following: " desc "" desc "- Runs the unit tests" desc "- Ensures Cocoapods compatibility" desc "- Bumps the minor version" lane :minor do update(type: "minor") end desc "This does the following: " desc "" desc "- Runs the unit tests" desc "- Ensures Cocoapods compatibility" desc "- Bumps the major version" lane :major do update(type: "major") end private_lane :update do |options| if $workspace scan( workspace: $workspace, scheme: $scheme, output_directory: "../Reports/", skip_slack: true ) else scan( scheme: $scheme, output_directory: "../Reports/", skip_slack: true ) end pod_lib_lint( allow_warnings: true ) type = options[:type] if type == "none" UI.message("No version type found") else version = version_bump_podspec(path: $podspec, bump_type: type) git_commit(path: $podspec, message: "Updating podspec") increment_version_number( bump_type: type, version_number: version, xcodeproj: $project ) commit_version_bump(xcodeproj: $project) post_to_slack(scheme: $scheme, version: version) end end private_lane :post_to_slack do |options| scheme = options[:scheme] version = options[:version] podname = scheme.upcase slack( message: "New `#{podname}` version: *#{version}* released :rocket:",) end
Submitting the Pod
Once the above actions have completed successfully, the Cocoapod is ready to submit.
The reason this is a separate step is that I use Git Flow to manage my branches. The above actions will happen on a release branch (e.g. release/3.1.4
) in case something goes wrong (e.g. a test fails) and needs to be fixed.
Once all the tests pass and the Cocoapod validates, it’s ready to be released. The release branch will be merged back in to master and tagged with the version number. This is important, as the Cocoapod can’t be pushed until the version listed in the .podspec
file matches a tag in the repository.
Here’s what the submit_pod
action will do:
- Push the master branch (and the new tag) to the remote repository
- Push the Cocoapod to a specifications repo (either the public master specifications repo, or a private repo defined as the
$specsrepo
variable in the CocoapodFastfile
) - Send a message via Slack
The lane itself looks like this:
desc "Push the repo to remote and submits the Pod to the given spec repository. Do this after running update to ensure that tests have been run, versions bumped, and changes committed." lane :submit_pod do |options| push_to_git_remote(local_branch: "master", remote_branch: "master", remote: "origin") # If a private specs repo is defined, use that instead. Otherwise use the master repo. if $specsrepo pod_push( path: $podspec, repo: $specsrepo, allow_warnings: true ) else pod_push( path: $podspec, allow_warnings: true ) end slack( message: "Pod submitted successfully! :rocket:", ) end
The Future
While I think Swift Packages are about to take over the dependency management world when it comes to Apple development, I can’t see Cocoapods disappearing any time soon. I am converting as many of my Cocoapod projects as I can to support SPM, but most of these (especially my public ones) will continue to also support Cocoapods for a while yet.
It’s useful to have a set of repeatable, automated actions and a clearly defined workflow to ensure that new versions continue to work as expected and I’ll be adapting these to support SPM as well.