Keeping on keeping my dependencies up to date

A little under two years ago, I wrote about a command line tool that I created to check that the dependencies in a swift package were up to date. It was mainly a pretext to learn the new (at the time) asynchronous stuff in Swift. This post is a followup about dusting a side project for no good reason.

What is Agamotto again?

If you’re not familiar with this command line tool that I was never able to release, well I’m not really surprised. The motivation described in the original blog post has not changed much: I made a command line tool that would fetch direct dependencies (so, not the transitive ones) in a Swift package using the swift command line tool. If those dependencies are on Github, the tool fetches the latest release using the API, then compare the version to the one used in the Package.swift file. It’s a pretty naive approach with a few limitations that I’m well aware of:

  • It only works for GitHub and no other SCMs, not even GitHub enterprise
  • It only works for public repositories
  • It only fetches the latest version on GitHub, no semantic versioning shenanigans

Fast forward to December where I had to travel back to France at the last minute. As I was looking for something to do during the journey, I opened a bunch of side projects and started doing some cleanup: fixing warnings, updating dependencies and configuring dependendabot with a single click. Pretty much immediately, I started getting notifications that dependabot started doing its job, making my tool pretty much obsolete. That being said, let’s look at the changes I made during those few days.

Does this code spark joy?

I started with a massive cleanup. Almost everything was under a single target, so I split that across various ones: GitHub Client, stuff to Interact with the Swift Package Manager, various helpers to run commands and read configuration… When you’re dusting an old side project, I highly recommend starting with that. It’s satisfying because chances are you’re learned new stuff since you wrote that code and you get the opportunity to correct the past. This is not something we often get to do when the project has expectations and deadlines.

Additionally, cleaning up all this code allowed me to tweak some of the mechanism around parsing the output of the Swift Package Manager command. I mentioned in the original blog post that changes in the format of the result to the command swift package describe caused the tool to break. To avoid that, I tweaked the filter passed to jq to something a little less strict. Now if a package doesn’t use exact versions, the command will not explode, and return null instead:

	.dependencies[].sourceControl[0] | 
		name: .identity, 
		cloneURL: .location.remote[0].urlString, 
		version: .requirement.exact?[0] 

There are still many reasons why this could break, but this is good enough for now. I’m quite proud that I was able to stop myself before I considered parsing the Package.swift file, remove the dependency on jq or something else that would have made the process more complicated for little added value.

One specification to rule them all

Once I was happy with the project structure, I started paying attention to my GitHub client. I have a particular fondness for GitHub and its REST API and I’ve always had. 10 years ago when I wrote my book, the application that I used as an example was using the GitHub jobs API (now defunct) to list open positions.

I like writing HTTP clients. It allows me to play with the type system a lot to perform encoding requests and decoding responses, it’s easy to test… Unfortunately for Agamotto, I only needed the one endpoint that lists releases for a given repository, so maintaining a client for this made little sense.

The good news is that since I started working on Agamotto, two things happened. First, GitHub finally released a somewhat-usable1 OpenAPI specification for their REST API. Second of all, Apple released a pretty fantastic tool to generate a Swift client based on a given OpenAPI specification. With that in mind, I downloaded the latest version of the specification, added the dependencies and the openapi-generator-config.yaml file required, and created a Makefile target to generate the GitHub client.

 swift run swift-openapi-generator generate \
  --mode types --mode client \
  --output-directory Sources/GitHubClient \
  --config Sources/GitHubClient/openapi-generator-config.yaml \

This is a pretty contentious topic but I’ve chosen to check the generated code into my repository for two reasons. The first one is that the specification file is massive, at close to 9Mb of YAML, so parsing this file takes a while and I don’t want to rely on Swift Package Manager to decide if it’s worth rebuilding or not. The second reason is that I only need to fetch the latest release for a given repository, so I tweaked the configuration file like so:

  - types
  - client
accessModifier: public  
    - repos/get-latest-release # The only operation we care about

The generated client is well under 100 lines of code (once you remove the comments). It is small enough to be checked into the repo and you can fight me on this2.

Did I break anything?

Finally, one last thing I never took the time to setup was continuous integration and deployment. I added simple unit tests for parsing the Package.swift file, but I would only run them manually when I realized the tool wasn’t working anymore after updating to a newer version of Swift. To this day, this oversight remains my greatest shame. Additionally, I was originally installing the tool with mint, a pretty awesome command line tool to build and install executables made with Swift, but I wanted to experiment with Homebrew as well.

As part of this whole adventure, I configured a couple of GitHub actions to:

  • Build and run tests on pull requests
  • Create a new release after merges on main. This involved re-running the tests, building the executable in release configuration and upload it to an S3 bucket, and create a GitHub release on the repository, with the generated artifact attached to it.
  • Finally, on a separate repository, I added one last GitHub action to create/update the Homebrew formula for the newly created release. It would grab the binary from the S3 bucket, compute the hash, update the formula and create a pull-request on my personal homebrew tap repository.

There are a few things that went really well with this process. First, GitHub Action’s matrix makes it trivial to run a job with multiple variations. In my case, that means building and running the tests on Swift 5.9 and 5.10, as well as macOS and Linux (Ubuntu) - since I wanted to experiment with Swift on Linux. This is probably overkill, but cool nonetheless.

I wouldn’t say that the last part (releasing with Homebrew) went wrong per-se, but it was definitely a little frustrating and took more efforts than what I was willing to put. While I rely on Homebrew to install tools on macOS all the time, I still find the documentation a little confusing when it comes to create formula. That being said, I managed to make it work, so I might write a new blog post dedicated to this. Meanwhile, I will keep using Mint to install the command line tool.

What now?

I was a little frustrated after I realized that I had done all this work for nothing, until I realized I didn’t. What started as a simple project to experiment with Swift’s “new” asynchronous API gave me the opportunity to experiment further with Swift Package Manager, jq, continuous integration and deployment of a command line tool written in Swift, using the openapi generator… I think it’s still one of my favourite sandbox to play in and I’m sure I can find ways to improve it further: adding configuration, support for other services such as Gitlab… Plus, I’m sure I can squeeze it for a few more blog posts ideas.

  1. While I’m happy they did, the spec has some limitations: it’s massive, breaks some tools 

  2. Please don’t actually fight me, I am trying to recover from tennis elbow and lower back pain right now 👴🏻