FubuMVC Lessons Learned: Semantic Versioning
TL;DR: I think that all .Net Nuget components should try to adopt Semantic Versioning, but it’s not all unicorns and rainbows
This series has a new home page at FubuMVC Lessons Learned. This post continues my discussion on componentized development in .Net that I started here and here. I’m going to write at least one or two more posts next week to seal this topic off.
Here’s the deal, one of the huge advantages of Nuget (or any other package manager) is the ability to more easily package up new releases as an OSS publisher and apply updates as a consumer. Great, but if things are changing and evolving a lot more frequently than when we got a new version of .Net every couple years, how are we gonna keep all of these different components working together with our application when it’s so much more common to have conflicting dependency versions that may or may not be actually compatible? I strongly believe that the .Net community should go all in and SemVer all the things!
If you’ve never heard of it, Semantic Versioning (SemVer) is a standard for versioning software libraries and tools that attempts to formalize rules for how to increment software versions based on compatibility and functionality. To summarize, a semantic version looks like MAJOR.MINOR.PATCH.BUILD, with the build number being optional. To simplify the SemVer rules:
- Anything version below 1.0 means that anything goes and you have no reason to expect any public API to be stable (making it to FubuMVC 1.0 was a very big deal to me last year).
- The major version should be incremented any time breaking changes are introduced
- The minor version should be incremented with any purely additive functionality
- The patch version should be incremented for backward compatible fixes
At the time I write this post, the current released version of StructureMap is 18.104.22.168. I introduced breaking changes to the public API in the initial 3.0 release, so StructureMap had to get a full point major release version which also resets the minor and patch versions. Since the 3.0 release I’ve had to make 3 different bug fix releases, but haven’t added any new functionality, so we’re up to 3.03. The last number (116) is the auto incrementing build number for traceability back to the exact revision in source control. If you adopted the original 3.0.0 version of StructureMap, you should be able to drop in any version less than some future 4.0 version and be confident that your code will still work assuming that StructureMap is following SemVer correctly.
Bundler and gems has specific support for expressing dependency constraints with SemVer like the following from FubuMVC’s Gemfile:
gem "rake", "~>10.0" gem "fuburake", "~>1.2"
The version constraint ~>1.2 is the equivalent in Nuget to using the dependency constraint [1.2, 2.0). I’d love to see Nuget adopt something like this operator for shorthand SemVer constraints, and then ask the .Net community to adopt this type of dependency versioning as a common idiom. I think I’d even like to see Nuget get some kind of mode where SemVer constraints are applied by default, such that Nuget warns you when you’re trying to install version 3.0 of StructureMap when one of your other dependencies declares a dependency on StructureMap 2.6.* because Nuget should be able to recognize that the latest version of StructureMap has potentially breaking changes.
SemVer and Backward Compatibility
On the publishing side of things, adopting SemVer is a way to be serious about communicating compatibility about older versions to our users. Adopting SemVer also makes you much more cognizant of the breaking changes you introduce into code.
In FubuMVC, adopting SemVer made us much more cautious about changing any kind of change to public API’s. The downside I think is that proposed improvements tend to collect for a lot longer before you bite the bullet and make a brand new major release. The 1.0 release was a huge source of stress for me because I was trying really hard to clean up all the public API’s before we locked them in and we ended up introducing a lot of changes in a short time that while I still think were very beneficial, made many of our users reticent to update. It also made us think very hard about spinning out many ancillary functions that were still evolving into separate libraries and repositories so that we could lock down the core of FubuMVC to 1.0 and continue to allow the downstream libraries to evolve faster with a pre 1.0 version. We did go too far and I’m reversing some of that in FubuMVC 2.0, but I still think that was a valuable exercise over all and I’d recommend that any large sprawling framework at least consider doing that.
Looking back, I think that community participation in FubuMVC clearly tapered off after the 1.0 release when so many of our users just stopped keeping up with the latest version and I’m still not sure what we could have done to improve that situation. If I had to do it all over again, I’m not sure I would have put so many eggs into the big SemVer 1.0 basket and just spent time doing documentation and quick starts.
The 2.0 release of FubuMVC is introducing fewer changes, but I’m still endeavoring to eliminate as much technical debt as possible and simplify the usage of major subsystems like content negotiation, authorization, and framework configuration — and that inevitably means that yep, there’s going to be quite a few breaking changes all at once. I’m not entirely sure if making so many breaking changes all at one time is a great idea, but neither was dribbling out occasional breaking changes before the 1.0 version.
The easiest thing is to just be perfect upfront, but since you’ll never actually pull that off in your framework, you have to figure out some sort of strategy for how you’ll introduce breaking changes.
Another way to put this is that you can either hold the line on backward compatibility and continue to annoy your users with all the ill informed early decisions over time or really piss off your users one time by fixing your usability issues. Pick your poison I guess. Or know your users. Do they want improvements more or do they value stability over everything else?
I started this series of posts about .Net componentization with the promise to the Nuget team that I’d write about what Ripple added to Nuget and my recommendations for Nuget itself, so that will be next. I’ve also got a much shorter post coming up on doing branch per feature with Nuget across multiple repositories.
Until next time, have a great weekend one and all….