Managing a Cocoapods Project with Fastlane

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:

  1. Run any tests that the Cocoapod might have
  2. Run pod lib lint to ensure the pod validates
  3. If they pass, bump the version number in the .podspec file based on the update type (i.e. major, minor, or patch) and store this new version number in a variable
  4. Commit this change to the .podspec file
  5. Bump the project version using the podspec variable, and bump the version of all of the targets
  6. Commit these changes to the project file and the Info.plist files of the targets
  7. 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:

  1. Push the master branch (and the new tag) to the remote repository
  2. Push the Cocoapod to a specifications repo (either the public master specifications repo, or a private repo defined as the $specsrepo variable in the Cocoapod Fastfile)
  3. 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.