You're not mature enough to release your first version as v1
This post's featured URL for sharing metadata is https://www.jvt.me/img/profile.jpg.
Last night I noticed that it's not possible to use semantic-release
to publish 0.x.y releases (aka "initial development versions") of packages.
What surprised me was that this isn't even configurable behind a switch - like how go-semantic-release
does it - but that you're just not able to do this at all, and instead have to make use of Semantic Versioning's pre-release versions for this functionality.
I find this rather puzzling because being able to publish a 0.x.y release is hugely powerful and useful, and should be the default, rather than recommending users go straight to a "stable" v1 release.
(I will say, I appreciate that as an Open Source project that's heavily used (and most likely underfunded) that they say "no" and make it clear that it isn't in scope for the project. That can exist alongside the frustration of not having this as a powerful feature, and has led to me not using the project for that reason)
I don't want to just focus on these two tools for defaulting to v1, as it's something I've seen before across projects, tools and people, and comes down to folks not considering how restrictive creating a stable API can be.
I've had some pretty talented engineers I've worked with in the past make the same mistake, releasing an internal library as 1.0.0, instead of 0.1.0, and it's something I've done as recently as this year π Versioning is hard, and so taking time to learn what it means to your project and its users is really important, in my opinion.
This is a post I've been looking to write since January, off the back of adding go-semantic-release
to dependency-management-data and realising that it didn't default to supporting "initial development versions", instead assuming that you wanted to release 1.0.0 as your first feature release.
This has been something that's been bubbling away in the back of my mind since my appearance on Go Time and our discussion about API versioning, as well as last night's discussion with Anders that cemented my need to get round to writing this post.
As tooling builders and library authors we should be embracing the ability to be "unstable", not shying away from it and rushing to release a "stable" version too early.
At the same time, we should ideally have a plan in mind for releasing a "stable" version at some point so consumers can avoid the risk of forever scanning CHANGELOG
s for possible breaking changes.
But it seems like not everyone is of the same opinion that releasing an "unstable" API should be the default, so what makes me feel so strongly?
You don't really know what you want
In the majority of cases, you don't really know what you want, and so sticking a "stable" label on it is disingenuous at best.
It's the same reason that humans are in the loop for building software, because there's a large difference between what our users say they want and actually what we need to solve for them. Because of this, you won't exactly know what the API should be for your users until it's been used a little more.
It's not entirely impossible, as you may have a very well-scoped problem you are solving, and have spent a lot of time discussing the problem and solution space, and exploring different options, but for most cases, it's unlikely.
As Anders mentions, your code doesn't exist in a vacuum, and can't actually really understand how well the interface works until you've had a number of different users, each with their own usecases, all of whom can help work out whether it's the right interface.
It's the reason I follow The Rule of Three rather than Don't Repeat Yourself (DRY) for how to deal with duplication in code I write. It's not until the third time that I actually see whether there's anything subtle to each case that needs to be taken into account, and the same is true for a library or tool that's used by a diverse set of users and for different purposes.
A small example of not actually knowing what you wanted too early is when when we released oapi-codegen/nullable
in January, and I went with v1.0.0, assuming that we'd "finished" the design, and were happy to call it "stable". And yet almost immediately, it was noted that we should remove the duplication in the name.
If we'd released as v0.1.0, got some initial feedback, and then finalised on v1.0.0, we could've fixed this before we needed to call it "stable", and fixed a glaring problem that'll stay with us until we get to a v2 release, if ever. Maybe we would've noticed this with a bit more code review, or some heavier linting rules, but that's just a good substitute for user feedback!
It's hard to design
In the same vein, designing an interface for your users is hard. Sometimes you think you know what you want, but the patterns you're using maybe aren't idiomatic, or don't necessarily make sense for your usecase.
You don't want to "paint yourself into a corner" and realise that you now have to keep those patterns around until the next breaking change (if it ever comes).
Instead, you should be giving yourself the flexibility in the future to adapt, and decide on your "stable" release when you're really confident it fits the usecases for your users.
It's a good communication tool
A fundamental value of versions are that they are a communication tool that are both machine- and human-readable.
It's not always the case that they're truly accurate, as there can be something that's missed by humans that actually is a breaking change, or there may some odd behaviour or a bug that someone now relies upon, and so any change can end up being treated as a "breaking change".
But it's very useful to be able to say:
hey, we're not yet at a stable release. There'll be some breaking changes introduced from time to time - sorry, but we'll try and signpost them as best as we can
By adopting a 0.x.y release, you're indicating exactly that to your users, and making it so they're aware you're not providing a "stable" API.
This can then be taken into account by your users to determine whether they want to risk using an "unstable" API and take on the burden of upgrades, or whether they want to find an alternate dependency to use.
Use your release notes to highlight breaking changes
While you're on an "unstable" version, a minor bump could be a case of you either having new feature(s) to ship, or it could be you've drastically rewritten everything and introduce a number of painful things to do before upgrading.
With a "stable" release, it's very clear as to whether there's a breaking change - is the major version bumped or not? But with an "unstable" release we need to be a bit more descriptive, as our version number is less expressive.
Because of this, you need to communicate in other means to your users.
For instance, in the go-semantic-release
driven notes that dependency-management-data uses, we can see that there's a separate section to indicate breaking changes, which provides a way to surface anything users need to be aware of before a release.
As well as using release notes for dependency-management-data, I also introduced a "compatible since" field which makes it possible to provide additional insight into version compatibility. This gives users greater insight into whether versions of the built database and command-line tools are compatible with each other, in addition to the version numbers within the tooling, while allowing the project to evolve with breaking changes and not yet settle on a "stable" release until it's really ready for it.
As noted in Breaking changes: a tooling problem, Richard highlights that there are two types of users: those who read the release notes in depth before an upgrade, and those who just look to see what breaks in their automated builds.
We can work hard to document the upgrade path where possible, so users are aware of what's affected, and any impact they're expected to see. But as noted by xkcd's Workflow, some functionality will be depended upon by users, regardless of whether it's something that should be used or not. So release notes - and any suggestions around whether something is a "breaking change" - are best effort, anyway.
And even if you have done the hard work to meticulously document everything, providing all the most relevant information for your users, who says they'll even read it? π
When we released the v2 release of oapi-codegen
, the release notes were clear around what impact a user may see, and a suggested upgrade path. But at GopherCon UK 2024 I had a chance to chat to one of the users of oapi-codegen
, who mentioned that their company haven't yet properly looked into what work was required for the v2 release (our first breaking change bump) we shipped last year. I found this a bit frustrating as there was basically no impact for users aside from bumping the module version, and we tried to do what we could to make it very clear for users.
It's an unfortunate fact that not everyone has time to read through release notes for each and every dependency they have, even if their dependency update tools, like Renovate, add them straight into the PRs.
It's part of Semantic Versioning
Another reason you should be relying upon these are because it's part of the spec and is in fact how the first version of the Semantic Versioning specification was released.
It's literally part of the framework we have for Semantic Version numbers, so why not take advantage of it?
Embrace the ability to evolve
There's a common saying along the lines of "how often have you looked back at code you wrote 6-12 months ago, and realised you could've written it better with hindsight?" and it's something I've felt so many times over my career.
As we build software, we learn more about the problem domain, and have much more powerful hindsight of how we could've done things differently.
It's incredibly restrictive to be tied to an API contract, and making the wrong choice can be a very long-lived mistake. I've felt this with some of the early contributions I made to oapi-codegen
that I now have to maintain, and rue the day I contributed them πΉ But then I read how Stripe does their API versioning, supporting every version of the API since company inception in 2011, and am very appreciative of the low API surface to maintain!
You'll naturally be maintaining APIs that you don't necessarily agree with, or have found don't work in practice over time. Being able to correct some of those mistakes you've made is nice, and is something you can do on an "unstable" API.
I'm sure it's very cathartic being able to "fix" years of API problems in one big release, but that's no reason to force yourself to pick up that rigid contract too early, and you should instead give yourself room to breathe, grow and evolve what your interface should look like.
I enjoy that with dependency-management-data I'm able to stay on an "unstable" API and really work hard to understand what "stable" means for us, and it gives a clear indication to our users that we're not yet "stable", but are working towards that.
Breaking changes are good when done in a controlled way, allowing you to experiment, correct, and evolve your model alongside user feedback.
This is key for library or tooling APIs
You may notice that I'm largely talking about libraries or tooling APIs, not i.e. Web APIs.
Web APIs are very different, and for the most part, I'd disregard this post in relation to them, and instead only focus on library APIs or tools.
It's super hard to push a backwards-incompatible change to a Web API, especially when it's an API that's public (i.e. there are users not internal to your organisation), because you can't as easily get in touch with everyone to say "here's the changes you need to upgrade to continue calling this API", whereas it's more easy when distributing a library to have your release notes populated in your dependency updating tool.
Someone pays for the complexity, somewhere
As noted in Shivam and Peter's talk at GopherCon 2024: Embracing complexity - Entropy in software design, there's always complexity somewhere.
The decision we make with an "unstable" API is that we want to make it possible to evolve and fix our API as we see fit over time, but unfortunately that then pushes the burden of an upgrade (and working out what breaking changes are introduced) onto our users. Whereas making a "stable" API then puts a lot of onus onto the maintainers, which can be very restrictive if done too soon.
You don't ever have to release v1
Although at some point, it's probably time to call it a day and declare that the API can now be considered "stable", it's also not a universal constant that you have to define stability.
Your users may prefer it, and there are some user or organisations who may shy away from your project if it's "unstable" for too long, but it's up to you - once you've stabilised, you're on the hook for maintaining it, warts and all.
Releasing v1, with a stable API, is a huge deal
It's also a hugely positive thing to be able to release your v1 release π
This is a great opportunity to celebrate, because you've gotten to the point where your project is widely used, you've iterated on your initial API, and are now happy to call it "stable".
As noted by the upcoming OpenPolicyAgent v1 release announcement, the team have been working on OPA for the best part of 9 years, at the time of writing my post. They've worked incredibly hard on providing a great toolset, and have been working towards their understanding of what a good "stable" version looks like. If they'd stablised too early, they'd likely have regrets, and it seems like now they've had suitable testing across their many APIs that they can say they're happy with it.
Enjoy that the moment comes from being able to say "we're done", and to be able to breathe a sigh of relief for you and your user.s